MTA CTF Writeup: Crypto 01

- Dạng bài: Crypto 
- Điểm : 1000 
- Kiến thức: AES-CBC, padding oracle, fixed IVs
Đề bài

I. Phân tích đề bài

"use strict";
const fs = require("fs");
const crypto = require("crypto");
const http = require("http");
const KEY = fs.readFileSync("key.txt");
const FLAG = fs.readFileSync("flag.txt", "utf8");
function generateToken(username) {
const payload = { time: Date.now(), user: username };
const cipher = crypto.createCipher("AES-192-CBC", KEY);
let token = cipher.update(JSON.stringify(payload), "binary", "hex");
token += cipher.final("hex");
return token;
}
function validateToken(token, username) {
if (!token) {
throw new Error("no token provided");
}
const decipher = crypto.createDecipher("AES-192-CBC", KEY);
let plaintext = decipher.update(token, "hex", "binary");
plaintext += decipher.final("binary");
const payload = JSON.parse(plaintext);
if (Date.now() - payload.time > 1000 * 60 * 60 * 24 * 30) {
throw new Error("token > 30 days old");
}
if (payload.user !== username) {
throw new Error("wrong user");
}
}
http
.createServer((req, res) => {
var URL = require('url').URL;
const url = new URL(req.url, "https://localhost:9000");
try {
switch (url.pathname) {
case "/":
res.writeHead(200, { "Content-Type": "text/plain" });
res.end(
`Welcome, anonymous. Your token is ${generateToken("anonymous")}.\nProvide your token on /flag?token=<token> to get flag!`
);
break;
case "/flag":
validateToken(url.searchParams.get("token"), "admin");
res.writeHead(200, { "Content-Type": "text/plain" });
res.end(FLAG);
break;
default:
res.writeHead(404, { "Content-Type": "text/plain" });
res.end("Not found");
}
} catch (err) {
res.writeHead(500, { "Content-Type": "text/plain" });
res.end(err.stack);
}
})
.listen(9000);
view raw server.js hosted with ❤ by GitHub

Đề bài cho file source được viết bằng nodejs. server được mở ra ở cổng 9000 tạo dịch vụ web http. Dịch vụ này cung cấp 2 endpoint, “/” và “/flag”.

http
  .createServer((req, res) => {
    var URL = require('url').URL;
    const url = new URL(req.url, "https://localhost:9000");
    try {
      switch (url.pathname) {
        case "/":
          res.writeHead(200, { "Content-Type": "text/plain" });
          res.end(
            `Welcome, anonymous. Your token is ${generateToken("anonymous")}.\nProvide your token on /flag?token=<token> to get flag!`
          );
          break;
        case "/flag":
          validateToken(url.searchParams.get("token"), "admin");
          res.writeHead(200, { "Content-Type": "text/plain" });
          res.end(FLAG);
          break;
        default:
          res.writeHead(404, { "Content-Type": "text/plain" });
          res.end("Not found");
      }
    } catch (err) {
      res.writeHead(500, { "Content-Type": "text/plain" });
      res.end(err.stack);
    }
  })
  .listen(9000);

Với endpoint “/”  sẽ thực thi function generateToken(“anonymous”) và trả về giá trị:

Khi truy cập vào endpoint “/”

Với endpoint  “/flag” thì sẽ truyền giá trị của parameter “token” vào function validateToken(url.searchParams.get(“token”), “admin”); sau khi thực hiện xong hàm này thì flag sẽ được trả về trong response.
– Phân tích function generateToken():

function generateToken(username) {
  const payload = { time: Date.now(), user: username };
  const cipher = crypto.createCipher("AES-192-CBC", KEY);
  let token = cipher.update(JSON.stringify(payload), "binary", "hex");
  token += cipher.final("hex");

  return token;
}

Function này sẽ qua các bước sau:
– tạo payload là một json có cấu trúc { time: Date.now(), user: username };  ví dụ
{ time :1598862188055, user : anonymous }
– tạo một cipher là hàm mã hóa AES-192-CBC với KEY là private
– chuyển payload sang string sau đó mã hóa bằng cipher  rồi encode hex và trả về kết quả.

Function  validateToken():

function validateToken(token, username) {
  if (!token) {
    throw new Error("no token provided");
  }
  const decipher = crypto.createDecipher("AES-192-CBC", KEY);
  let plaintext = decipher.update(token, "hex", "binary");
  plaintext += decipher.final("binary");
  const payload = JSON.parse(plaintext);

  if (Date.now() - payload.time > 1000 * 60 * 60 * 24 * 30) {
    throw new Error("token > 30 days old");
  }

  if (payload.user !== username) {
    throw new Error("wrong user");
  }
}

Function này sẽ  giải mã token ra sau đó tách trường user và so sanh với username được truyền vào. Nếu user này mà khác username thì tạo ra một Error. Và đương nhiên khi có error thì không thể nhận lại được flag.


Kết luận: Đề cho token là mã hóa  AES-CBC của đoạn “anonymous” , mục dích là thay đổi giá trị user trong token thành “admin”

II Khai thác lỗ hổng của mã hóa AES

Nhận thấy phép mã hóa sử dụng mã hoá AES-192-CBC (Nếu chưa biết mã hóa AES-CBC là gì có thể đọc ở đây) . Về cơ bản AES-CBC được mô tả qua hai hình sau:

Cipher block chaining (CBC) mode encryption
https://upload.wikimedia.org/wikipedia/commons/thumb/2/2a/CBC_decryption.svg/1920px-CBC_decryption.svg.png

Trong quá trình giải mã P3 = D(C3) ^ C2. Nếu ta thêm một block Ct vào giữa C2 và C3 thì sẽ thêm Pt là decrypt của Ct.  Giá trị của P3 sẽ phụ thuộc vào Ct được thêm vào trong khi giá trị của P2 sẽ không thay đổi. Điều này có ý nghĩa đối với Plaintext là 1 json, đoạn Pt được inject vào sẽ thuộc 1 string nào đó thì hoàn toàn json này có ý nghĩa. VD: {“time”:1574477443310,”user”:”anRANDOM_JUNK_HEREonymous“} đoạn RANDOM_JUNK_HERE đã được inject vào token mà vẫn đúng cấu trúc.

Sơ đồ tổng quát quá trình inject


Lợi dụng tính chất của CBC ta hoàn toàn có thể inject một đoạn mới có trương user là admin vào token. Với đoạn token chưa được mã hóa {“time”:1598862188055,”user”:”anonymous”} sau khi chia block 16 byte thì block cuối có giá trị là onymous”} kết hợp với padding ta có đoạn hoàn chỉnh onymous”}0x070x070x070x070x070x070x07, cần biết đổi block này thành dạng “,”user”:”admin”}0x01 để bypass quá trình padding của AES.  Tuy nhiên độ dài của đoạn dữ liệu cần inject lên đơn 18 byte trong khi mỗi khối có 16 byte vậy cần inject vào 2 khối. Với khối thứ hai Pt2: [random_char(14)]“, và khối C3 decrypt ra Pt3: “user”:”admin”}0x01. Lúc đó chuỗi token sẽ thành {“time”:1574477443310,”user”:”an[16+14 byte random]”,”user”:”admin”} Với khối khối thứ 2 hoàn toàn có thể tính bằng công thức sau

Ct2 = C2^P3^Pt3

Với Ct2 được truyền vào thì quá trình dycrypt sẽ sinh ra Pt2‘. Nhưng ta mong muốn Pt2 = Pt2‘ vậy nên áp dụng tương tự ta có công thứ tính Ct1

Ct1 = Pt2‘^C2^Pt2

Để tính được Ct1 ta cần phải biết được giá trị của Pt2‘. Padding orcale có thể giải quyết vấn đền đó. Tiến hành xây dựng sơ đồ như hình sau. Sử dụng padding oracle tìm ra Pt2

Sơ đồ quá trình padding oracle tìm ra Pt2′

Padding oracle

Nếu chưa biết padding oracle là gì có thể đọc ở bài viết này. Nhận thấy khi thay đổi giá trị token thì có exception trả về. Nhận thấy 2 dòng quan trọng 22 và 24 sẽ trả về exception nếu đầu vào sai định dạng.

response khi token sai định dạng encrpy
response khi token sai định dạng JSON

Từ đây xây dựng ra script để có thể tính toán giá trị của đoan Pt2‘. Sau khi biết được đoạn Pt2‘ hoàn toàn có thể tính ra đoạn Ct1 để sao cho Pt2 là giá trị mong muốn.

Chú ý: Giá trị của đoạn Pt1 sau khi decrypt có thể chưa vài ký tự làm cho JSON bị sai câu trúc vậy nên cần lặp lại quá trình sinh ramdom 14 ký tự để thu được kết quả là JSON thỏa mãn.

import re
import requests
import time
import random
def get_random_string(length):
letters = "".join(chr(i) for i in range(256))
result_str = ''.join(random.choice(letters) for i in range(length))
return result_str
url = "http://bu.gbounty.cc:9000&quot;
def gettoken(url):
print "[+] Get token: ",
text = requests.get(url).text
regex = "[a-f0-9]{96}"
token = re.findall(regex, text)[0]
print token
return token
def check(token):
req = requests.get(url + '/flag?token='+ token)
if 'bad decrypt' not in req.text:
if 'JSON.parse' in req.text:
return 1
else:
return 2
else:
return 0
def xor(a, b):
ans = ""
for i in range(len(a)):
ans += chr(ord(a[i])^ord(b[i]))
return ans
def padding_oracle(url, token, block, blocksize):
start_time = time.time()
le = token[:block-blocksize]
re = token[block:]
hv = ""
for vt in range(16):
print '[*] Find: ', vt
print '[-] Time: ', time.time() - start_time , 's'
for i in range(256):
tk = (le + token[block-blocksize:block-vt-1] + chr(i) + xor(hv, chr(vt+1)*vt) + re).encode('hex')
ans = check(tk)
if (ans == 1):
hv = chr(i^(vt+1)) + hv
break
print "[+] Stop padding padding oracle: " , time.time() - start_time, "s"
print "[+] answer: ", hv.encode('hex')
return hv
def main():
token = gettoken(url).decode('hex')
token_po = token[:32] + xor(xor(token[16:32], 'onymous"}' + chr(7)*7) , '"user":"admin"}' + chr(1))
ans_padding = padding_oracle(url, token_po, 32, 16);
while(1):
token_check = token[:32] + xor(ans_padding, get_random_string(14) + '",')+ xor( xor(token[16:32],'onymous"}' + chr(7)*7 ), '"user":"admin"}' + chr(1) ) +token[32:]
ans = check(token_check.encode('hex'))
if ans == 2:
print "[+] Success: ", token_check.encode('hex')
print "[+] Flag: " , requests.get(url + '/flag?token=' + token_check.encode('hex')).text
break
#66d6f1b9008a352c4d44022bb4487ac9050787954232786faebdaace3cbedd5ad032215e03dfd0337f3d80f775398be8481c8d9d5f65316fb2dec0a0559ba75c2be2aee3f3248d13763493bb4e6372fb
if __name__ == '__main__':
main();
view raw exploit.py hosted with ❤ by GitHub

Sau một quá trình chờ đợi khá là dài gần 21 phút :((  (Nên dựng server trên local test trước khi test với server gốc)

Kết quả đạt được 

Thời gian chạy: 1297s
Token: 0feddec397d4688967e9c3515f929799fba0284ae14e29003419a750c6e26ee3cef4fa96324ee3044812f5e3930aec1fb6bb2242fc196000287acd3eafc714e589dc07be24716ac695cd0b92d8a87b2a
URL: http://bu.gbounty.cc:9000/flag?token=0feddec397d4688967e9c3515f929799fba0284ae14e29003419a750c6e26ee3cef4fa96324ee3044812f5e3930aec1fb6bb2242fc196000287acd3eafc714e589dc07be24716ac695cd0b92d8a87b2a
Flag: mtactf{padding_oracle_never_die}

Bài viết tham khảo:

https://blog.teddykatz.com/2019/11/23/json-padding-oracles.html
https://nsbvc.blogspot.com/2019/01/vua-ngo-ra-su-vi-dieu-cua-padding.html
https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation

Trả lời

Mời bạn điền thông tin vào ô dưới đây hoặc kích vào một biểu tượng để đăng nhập:

WordPress.com Logo

Bạn đang bình luận bằng tài khoản WordPress.com Đăng xuất /  Thay đổi )

Google photo

Bạn đang bình luận bằng tài khoản Google Đăng xuất /  Thay đổi )

Twitter picture

Bạn đang bình luận bằng tài khoản Twitter Đăng xuất /  Thay đổi )

Facebook photo

Bạn đang bình luận bằng tài khoản Facebook Đăng xuất /  Thay đổi )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.