🧬TooManyCrypto

Medium

Overview

Xem qua bài này thì trang web có hai chức năng, đó là encrypt và decrypt một đoạn text nào đó mà bạn nhập vào.

Analys

Chúng ta xem qua trang web có gì khác biệt có thể có vuln không nhé.

Chú ý vào url khi chúng ta chuyển tab encrypt và decrypt:

http://localhost:9001/?page=decrypt

Mình đoán đây là LFI hay Path Traversal. Ở đây khi chuyển các tab thì page = decrypt.php. Mình nghĩ vậy.

Ở phía server chắc sẽ include("$page".".php") -> nó sẽ được nối với php để load lên.

Chính xác!!! Chúng ta cần lấy source của các trang để có thể exploit được.

Mình đã bypass được đầu vào để đọc source của index -> dùng Php, ở phía server chắc đã chặn php....(Có thể là vậy).

Chúng ta sẽ xem index có gì:

   <?php if (isset($_GET["page"]) && !empty($_GET["page"])) {
       $page = $_GET["page"];
       if (strpos(strtolower($page), "secret") !== false) {
           die("<center><img src='./images/wrongway.jpg'/></center>");
       } elseif (strpos($page, "php") !== false) {
           die("<center><img src='./images/baka.gif'/></center>");
       } else {
           include $page . ".php";
       }
   }

?>

Đúng như chúng ta dự đoán phía đầu vào có lọc php và hình như có file secret.php nữa-> nó sẽ không có cách nào đọc được.

Bây giờ chúng ta xem đoạn code decrypt:

<?php

include('./secret.php');

if(empty($_SESSION["form_token"]))
{
$gen_token=md5(uniqid(rand(), true));
$_SESSION["form_token"] = $gen_token;
//header("Refresh:0");
}

if(isset($_POST["dec"]) && isset($_POST["token"]))
{
    if($_SESSION["form_token"]===$_POST["token"])
    {
        unset($_SESSION['form_token']);
        $gen_token=md5(uniqid(rand(), true));
        $_SESSION["form_token"] = $gen_token;
        if(base64_decode($_POST["dec"]))
        {
        $final=$_POST['dec'];
        $decrypted3=base64_decode($final);
        $decrypted2=tsu_super_decrypt3($decrypted3);
        $decrypted1=tsu_super_decrypt2($decrypted2);
        $decrypted0=tsu_super_decrypt1($decrypted1,$key);
            if(tsu_super_decrypt0($decrypted0))
            {
            $plaintext=tsu_super_decrypt0($decrypted0);
            echo '<pre><font color="red">Your message....here...baka: </font><font color="blue">'.htmlentities(substr($plaintext,73)).'</font></pre>';
            }
            else
            {
            echo '<pre><font color="red">Onii-chan!!! I Cannot Decrypt It!</font></pre>';
            }
        }
        else
        {
            echo '<pre><font color="red">Hey Onii-chan, Wrong Input! Base64 Only Baka-desu</font></pre>';
        }
    }
}


?>

Mình sẽ chỉ nó sơ qua đoạn code decrypt.php này thì đơn giản là nó giải mã lại tất cả những gì hàm encrypt đã làm và có một điều mỗi form đều có token-> nó tránh chúng ta brute force.

Đây là đoạn code encrypt.php

<?php
//$flag=$secret_salt;
include('./secret.php');

if(empty($_SESSION["form_token"]))
{
$gen_token=md5(uniqid(rand(), true));
$_SESSION["form_token"] = $gen_token;
//header("Refresh:0");
}

function tsu_super_encrypt0($c)
{
  return gzcompress($c,-1);
}

function tsu_super_encrypt1($c,$key)
{
    $l=strlen($key);
    $string="";
    for($i=0;$i<strlen($c);$i++)
    {
        $string[$i]=chr((ord($c[$i]) | ord($key[$i%$l])) & (256+~(ord($c[$i]) & ord($key[$i%$l])))%256);
    }
    return implode("",$string);
}

function tsu_super_encrypt2($c)
{
    $l=strlen($c);
    $string="";
    for($i=0;$i<$l;$i++)
    {
        $string[$i]=chr((ord($c[$i])+$i)%256);
    }
    return implode("",$string);
}

function tsu_super_encrypt3($c)
{
  $l=strlen($c);
  $k=$l%8;
  $string="";
  for($i=0;$i<$l;$i++)
  {
  $string[$i]=chr(((ord($c[$i])<<$k)|ord($c[$i])>>(8-$k))&0xff);
  }
  return implode("",$string);
}

?>

<?php

if(isset($_POST["enc"]) && strlen($_POST["enc"]) && isset($_POST["token"]))
{
  if($_SESSION["form_token"]===$_POST["token"])
  {
  unset($_SESSION['form_token']);
  $gen_token=md5(uniqid(rand(), true));
  $_SESSION["form_token"] = $gen_token;
  $enc=$_POST["enc"];
  $query="secret=".$secret_salt."string=".$enc;// secret = flag.string=<input>
  $encrypted0=tsu_super_encrypt0($query);
  $encrypted1=tsu_super_encrypt1($encrypted0,$key);
  $encrypted2=tsu_super_encrypt2($encrypted1);
  $encrypted3=tsu_super_encrypt3($encrypted2); //I'm too sleepy, i think i should stop here..., oyasuminasai...mm..mm..zz..
  $final=base64_encode($encrypted3);
  echo '<pre><font color="red">Hey onii-chan...Here is your crypt...</font><font color="blue">'.$final.'</font></pre>'; 
  
  }
}
?>

Đoạn code này bao gồm một số chức năng để mã hóa một chuỗi văn bản và trả về kết quả dưới dạng một chuỗi được mã hóa đặc biệt. Bên cạnh đó là một số phần khác để tạo và kiểm tra mã thông báo biểu mẫu (form token), giúp bảo vệ trang khỏi tấn công giả mạo yêu cầu (CSRF).

Đoạn code bắt đầu bằng cách bao gồm tệp secret.php và thiết lập mã thông báo biểu mẫu (form token) nếu nó chưa tồn tại trong phiên hiện tại. Mã thông báo biểu mẫu được tạo bằng cách sử dụng hàm md5() để tạo một chuỗi băm từ một chuỗi ngẫu nhiên được tạo bằng hàm uniqid().

Sau đó, đoạn mã định nghĩa một loạt các hàm mã hóa, bao gồm tsu_super_encrypt0(), tsu_super_encrypt1(), tsu_super_encrypt2()tsu_super_encrypt3(). Các hàm này được sử dụng để thực hiện các bước mã hóa khác nhau trên chuỗi đầu vào.

Hàm tsu_super_encrypt0() sử dụng hàm gzcompress() để nén chuỗi đầu vào bằng thuật toán nén gzip.

Hàm tsu_super_encrypt1() thực hiện việc mã hóa chuỗi đầu vào bằng cách thực hiện các phép toán bit trên từng ký tự của chuỗi đầu vào và từng ký tự của khóa được cung cấp. Kết quả được lưu trữ trong một chuỗi mới.

Hàm tsu_super_encrypt2() thực hiện mã hóa ký tự bằng cách thay thế từng ký tự trong chuỗi đầu vào bằng một ký tự mới được tính toán bằng cách thêm một giá trị vào mã ASCII của ký tự đó.

Hàm tsu_super_encrypt3() thực hiện việc mã hóa chuỗi đầu vào bằng cách thực hiện phép dịch bit trái hoặc phải trên từng ký tự của chuỗi đầu vào và kết hợp với ký tự tiếp theo.

Sau khi các hàm mã hóa đã được định nghĩa, đoạn mã xử lý các yêu cầu POST được gửi từ biểu mẫu. Nếu yêu cầu POST chứa một chuỗi được mã hóa (enc) và mã thông báo biểu mẫu (form token) khớp với phiên hiện tại, thì chuỗi được giải mã, mã hóa và mã hóa lại bằng các hàm đã định nghĩa. Kết quả cuối cùng được mã hóa base64 và trả về cho người dùng dưới dạng chuỗi được mã hóa đặc biệt.

Oke cái quan trọng là chúng là không có $key và nó nằm ở trong file secret.php nên từ function tsu_super_encrypt1($c,$key) xuống hết đoạn code php thứ nhất chúng ta sẽ bỏ qua.

Chúng ta sẽ xem xét hàm function tsu_super_encrypt1($c) nó sẽ được mã hóa bằng hàm gzcompress()

Mình sẽ nói về hàm gzcompress() này:(Đánh dấu cái này là * nhé)

gzcompress() là một hàm trong PHP được sử dụng để nén dữ liệu bằng thuật toán nén gzip. Hàm này có thể nén chuỗi dữ liệu thành một chuỗi nhị phân được mã hóa theo chuẩn gzip.

Cú pháp của hàm gzcompress() như sau:

string gzcompress ( string $data [, int $level = -1 [, int $encoding = ZLIB_ENCODING_DEFLATE ]] )

Trong đó:

  • $data: Chuỗi dữ liệu cần nén.

  • $level: Mức độ nén dữ liệu. Giá trị -1 mặc định sẽ sử dụng mức độ nén tối ưu nhất.

  • $encoding: Các thuật toán nén khác nhau có sẵn trong thư viện zlib. Giá trị mặc định là ZLIB_ENCODING_DEFLATE, tương ứng với thuật toán nén deflate.

Hàm gzcompress() trả về một chuỗi nhị phân đã được nén. Để giải nén chuỗi này, bạn có thể sử dụng hàm gzuncompress() hoặc gzinflate().

Được biết rằng thì hàm này nếu $encoding nếu không điền gì thì mặc định của nó là ZLIB_ENCODING_DEFLATEE . Chúng ta sẽ nói về thuật toán deflatee này sau.

Tiếp theo tới đoạn php thứ hai trong file encrypt.php

$query="secret=".$secret_salt."string=".$enc;// secret = flag.string=<input>

Chú ý đoạn code trên nó sẽ gán chuỗi secret=flag.string=<input> như trên đoạn code $secret_salt thì nó là flag.

Ví dụ: Bạn nhấp một đoạn trong form: moscowuniverse

-> Khi đó biến query nó sẽ như này: secret=flag.string=moscowuniverse

Chúng ta sẽ nói thuật toán Deflatee:

DEFLATE là một thuật toán nén dữ liệu được sử dụng rộng rãi trong các định dạng tập tin như ZIP và gzip. Nó kết hợp hai kỹ thuật nén dữ liệu khác nhau là huffman coding và lz77, để tạo ra một kết quả nén hiệu quả hơn.

Cơ bản, thuật toán DEFLATE hoạt động như sau:

  1. Sử dụng thuật toán LZ77 để tìm kiếm chuỗi dữ liệu trùng lặp và thay thế chúng bằng các cặp "lenght-distance". Cặp này bao gồm một số nguyên đại diện cho độ dài của chuỗi trùng lặp và một số nguyên đại diện cho khoảng cách từ vị trí hiện tại đến vị trí đầu tiên của chuỗi trùng lặp đó trong tệp.

  2. Sử dụng thuật toán Huffman coding để mã hóa các ký tự trong các khối dữ liệu còn lại sau khi đã áp dụng thuật toán LZ77. Huffman coding sử dụng bảng tần số để gán các mã mã hóa ngắn hơn cho các ký tự xuất hiện thường xuyên hơn. Điều này giúp giảm kích thước của các khối dữ liệu và tăng tốc độ nén.

  3. Đưa các cặp length-distance và các khối dữ liệu đã được mã hóa Huffman vào một khối dữ liệu đầu ra, được sắp xếp theo thứ tự nhất định và có thêm các thông tin điều khiển như độ dài và CRC (cyclic redundancy check) để đảm bảo tính toàn vẹn của dữ liệu.

  4. Cuối cùng, các khối dữ liệu được nối lại với nhau để tạo thành tệp nén DEFLATE.

Tổng quát, DEFLATE là một thuật toán nén dữ liệu mạnh mẽ và hiệu quả, kết hợp hai kỹ thuật nén khác nhau để tạo ra kết quả nén tối ưu hơn.

Chú ý: lenght-distance

Ví dụ cho dễ hiểu sẽ như này:

Giả sử chúng ta có một chuỗi ký tự ban đầu là "AAABBBCCCDDD". Để nén chuỗi này bằng thuật toán Deflate, chúng ta thực hiện các bước sau:

  1. Sử dụng thuật toán LZ77 để tìm kiếm các chuỗi trùng lặp trong chuỗi ký tự ban đầu. Trong ví dụ này, chúng ta có chuỗi "AAA", "BBB" và "DDD" được tìm thấy.

  2. Tạo ra một bảng mã hóa cho các chuỗi trùng lặp được tìm thấy. Các chuỗi này được mã hóa thành một cặp (l, d), trong đó l là độ dài của chuỗi trùng lặp và d là khoảng cách giữa chuỗi trùng lặp và vị trí hiện tại. Trong ví dụ này, chúng ta có các cặp (3, 0) cho chuỗi "AAA", (3, 3) cho chuỗi "BBB" và (3, 9) cho chuỗi "DDD".

  3. Tạo ra một bảng mã hóa cho các ký tự không trùng lặp. Các ký tự này được mã hóa bằng cách sử dụng mã hóa Huffman, trong đó các ký tự xuất hiện nhiều hơn sẽ có độ dài mã hóa ngắn hơn. Trong ví dụ này, chúng ta có các ký tự "C" xuất hiện 3 lần, "B" xuất hiện 3 lần và "A" xuất hiện 3 lần.

  4. Ghép các cặp (l, d) và ký tự đã được mã hóa thành một chuỗi nén. Trong ví dụ này, chuỗi nén sẽ là "AAA BBB CCC DDD", với các cặp (3, 0), (3, 3), (3, 9) và các ký tự "C", "B", "A" được mã hóa bằng mã hóa Huffman.

  5. Thêm các thông tin đầu vào, bao gồm bảng mã hóa cho các chuỗi trùng lặp và ký tự không trùng lặp, vào đầu chuỗi nén. Điều này cho phép quá trình giải nén có thể giải mã chuỗi nén thành chuỗi ký tự ban đầu.

Như vậy, chuỗi "AAABBBCCCDDD" đã được nén thành chuỗi "AAA BBB CCC DDD" bằng thuật toán Deflate. Quá trình giải nén sẽ tiến hành các bước ngược lại để giải nén chuỗi nén thành chuỗi ký tự ban đầu.

Chúng ta sẽ dùng kỹ thuật crime attack, nó sẽ sử dụng so sánh độ dài chuỗi từ thuật toán deflate để dump ra từng ký tự.

Như chúng ta đã biết $query="secret=".$secret_salt."string=".$enc . Nếu như đầu vào của chúng ta nhập giống với chuỗi secret=".$secret_salt."string .

Từ ví dụ và cách thức hoạt động trên thì chúng ta sẽ ghép từng char của $enc một trùng với đoạn string phía trước, khi đó length của chuỗi hay có thể gọi là khoảng cách của chữ lặp lại của nó sẽ không thay đổi.

secret=MeeCTF{hoahongdo}.string=secret= -> secret=MeeCTF{hoahongdo}.string=(7,32)

Khi đó nếu như độ dài chuỗi khi được encrypt theo thuật toán Deflate sau khi thêm ký tự vẫn bằng độ dài ban đầu thì chứng tỏ chúng ta đã nhập đúng ký tự lặp lại của chuỗi trc

secret=".$secret_salt."string=<input> -> input sẽ tự điền các ký tự trong flag.

Exploit

Như đã trình bày ở trên thì nói qua về ý tưởng nếu $enc chúng ta nhập có ký tự trùng với chuỗi phía trước bao gồm flag thì khi đó độ dài chuỗi hay có thể nói là khoảng cách vẫn giữ nguyên.

secret=MCTF{xxxx}=secret= -> đoạn này sau khi bị compress thì độ dài như cũ -> tiếp tục thêm ký tự trùng phía trước.

Có thể thấy rằng sau dấu = thì chữ M có length là 84. Bây giờ chúng ta sẽ lấy cặp key:value mà có value là 84 gắn vào flag.

Payload exploit sẽ như sau:

import requests
import base64
import re

# Hàm encrypt() thực hiện mã hóa một thông điệp msg và trả về chuỗi kết quả mã hóa
def encrypt(msg):
    # Gửi yêu cầu GET đến trang mã hóa
    url_enc = "http://localhost:9001/?page=encrypt" 
    s = requests.session()
    r = s.get(url_enc)

    # Tìm kiếm token từ phản hồi HTML
    regex_token = '<input type="hidden" name="token" value=(?P<token>.*) />'
    m = re.findall(regex_token, r.text)
    token = m[0]

    # Gửi yêu cầu POST với thông điệp cần mã hóa và token
    data = { "enc":msg, "token":token}
    r = s.post(url_enc, data=data)

    # Tìm kiếm chuỗi kết quả mã hóa từ phản hồi HTML
    regex_enc = 'Here is your crypt...</font><font color="blue">(?P<enc>.*)</font>'
    m = re.findall(regex_enc, r.text)
    enc = m[0]
    return enc


# Hàm check_length() giải mã chuỗi kết quả mã hóa và trả về chiều dài của chuỗi giải mã
def check_length(enc):
    tmp = base64.b64decode(enc)
    return len(tmp)


# Chuỗi ký tự được sử dụng để tìm kiếm mật khẩu
charset = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_{}'

# Biến chứa một phần của mật khẩu được tìm kiếm
flag = 'secret='

# Từ điển tạm thời để lưu trữ các giá trị được tính toán trong quá trình tìm kiếm mật khẩu
tmp_dict = {}

# Tìm kiếm mật khẩu bằng kỹ thuật Brute Force
for i in range(0,500):
    for char in charset:
        # Thêm ký tự char vào phần đã biết của mật khẩu để tạo thành thông điệp cần mã hóa
        plain_text = flag + char

        # Mã hóa thông điệp và tính toán chiều dài của chuỗi giải mã
        enc = encrypt(plain_text)
        tmp_dict[char] = check_length(enc)

    # Thêm ký tự có chiều dài giải mã nhỏ nhất vào flag
    flag+= min(tmp_dict.items(), key = lambda x:x[1])[0]
    print(flag)

    # Nếu phần cuối cùng của flag là "}", dừng tìm kiếm
    if flag[-1] == "}": break

Last updated