+3

[Write-up] Hackthebox: Clicker. Nếu Server mở dịch vụ NFS thì có cách nào để khai thác không?

OS Difficulty
Linux Medium

Giới thiệu

Clicker là 1 machine trên Hackthebox có độ khó ở mức Medium. Bài cho chúng ta biết cách thực hiện pentest như nào khi mà trên server có mở cổng 2049(NFS).

1. Recon

Sử dụng Nmap để kiểm tra dịch vụ chạy trên Server

┌──(kali㉿kali)-[~]
└─$ sudo nmap -sC -sV 10.10.11.232
[sudo] password for kali: 
Starting Nmap 7.92 ( https://nmap.org ) at 2024-01-03 03:51 EST
Nmap scan report for clicker.htb (10.10.11.232)
Host is up (0.091s latency).
Not shown: 996 closed tcp ports (reset)
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 89:d7:39:34:58:a0:ea:a1:db:c1:3d:14:ec:5d:5a:92 (ECDSA)
|_  256 b4:da:8d:af:65:9c:bb:f0:71:d5:13:50:ed:d8:11:30 (ED25519)
80/tcp   open  http    Apache httpd 2.4.52 ((Ubuntu))
|_http-title: Clicker - The Game
| http-cookie-flags: 
|   /: 
|     PHPSESSID: 
|_      httponly flag not set
|_http-server-header: Apache/2.4.52 (Ubuntu)
111/tcp  open  rpcbind 2-4 (RPC #100000)
| rpcinfo: 
|   program version    port/proto  service
|   100000  2,3,4        111/tcp   rpcbind
|   100000  2,3,4        111/udp   rpcbind
|   100000  3,4          111/tcp6  rpcbind
|   100000  3,4          111/udp6  rpcbind
|   100003  3,4         2049/tcp   nfs
|   100003  3,4         2049/tcp6  nfs
|   100005  1,2,3      41995/tcp6  mountd
|   100005  1,2,3      49388/udp   mountd
|   100005  1,2,3      57925/udp6  mountd
|   100005  1,2,3      60761/tcp   mountd
|   100021  1,3,4      35949/udp6  nlockmgr
|   100021  1,3,4      40341/tcp6  nlockmgr
|   100021  1,3,4      40645/tcp   nlockmgr
|   100021  1,3,4      44662/udp   nlockmgr
|   100024  1          32806/udp6  status
|   100024  1          48193/tcp6  status
|   100024  1          49263/udp   status
|   100024  1          59853/tcp   status
|   100227  3           2049/tcp   nfs_acl
|_  100227  3           2049/tcp6  nfs_acl
2049/tcp open  nfs_acl 3 (RPC #100227)
|_http-title: Directory listing for /
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 19.31 seconds

Chúng ta được 4 cổng 22: SSH, 80: HTTP, 111: rcpbind, 2049: NFS

Thực hiện thêm dòng sau vào /etc/hosts

10.10.11.232          clicker.htb

2. Enum

Thực hiện kiểm tra ứng dụng Web

image.png

Thì nó là 1 game với số điểm bằng lượng click của người chơi

image.png

Sau khi kiểm tra thì chưa phát hiện được chỗ nào có thể khai thác được

Tiếp theo kiểm tra đến cổng 2049 thì nó là dịch vụ NFS giúp chia sẻ file lên các network

Sau khi google thì ta được kết quả tại đây. Nó có hướng dẫn cách pentest 1 ứng dụng chạy NFS

Mình sẽ thực hiện như sau:

Đầu tiên kiểm tra thư mục trên server có public để chúng ta mount được không

┌──(kali㉿kali)-[~]
└─$ showmount -e 10.10.11.232          
Export list for 10.10.11.232:
/mnt/backups *

Chúng ta có thư mục /mnt/backups ở trên server có thể mount được

Mình tạo 1 thự mục ở đây là clicker

Thực hiện mount thư mục /mnt/backups lên thư mục ở trên máy mình là clicker

sudo mount -t nfs 10.10.11.232:/mnt/backups clicker/

Thực hiện xem trong thư mục t vừa mount thì có 1 file zip tên là clicker.htb_backup.zip .Thực hiện giải nén ta được Source Code của ứng dụng web của game clicker ở trên mình truy cập, các chức năng được thực hiện như sau:

  • admin.php: trang quản trị của admin(cần quyền admin)
  • authenticate.php: trang thực hiện xác thực hiện quá trình đăng nhập
  • db_utils.php: trang thực hiện các bước liên quan đến database
  • diagnostic.php: nó cần token nên mình không quan tâm đến nó
  • export.php: xuất ra file (cần quyền admin)
  • info.php: hiển thị thông tin về ứng dụng
  • login.php: trang đăng nhập
  • logout.php: trang đăng xuất
  • play.php: trang chơi game
  • profile.php: hiển thị thông tin của mình
  • register.php: trang đăng ký
  • save_game.php: lưu game

3. Exploit

Phân tích chức năng

Sau khi thực hiện đọc code cũng như là tìm hiểu các chức năng của ứng dụng thì mình thấy các file có thể khai thác như sau:

# save_game.php
<?php
session_start();
include_once("db_utils.php");

if (isset($_SESSION['PLAYER']) && $_SESSION['PLAYER'] != "") {
	$args = [];
	foreach($_GET as $key=>$value) {
		if (strtolower($key) === 'role') {
			// prevent malicious users to modify role
			header('Location: /index.php?err=Malicious activity detected!');
			die;
		}
		$args[$key] = $value;
	}
	save_profile($_SESSION['PLAYER'], $_GET);
	// update session info
	$_SESSION['CLICKS'] = $_GET['clicks'];
	$_SESSION['LEVEL'] = $_GET['level'];
	header('Location: /index.php?msg=Game has been saved!');
	
}
?>

Chức năng của file này sẽ thực hiện nhận input của người dùng tương đương với 1 mảng, nếu trong mảng đó có từ role thì sẽ bị chặn và hiển thi thông báo Malicious activity detected!. Nếu thoả mãn điều kiện if (nghĩa là không có từ role), nó sẽ set giá trị vào trong mảng $arg

Sau đó sẽ thực hiện save_profile() và hiển thị thông báo Game has been saved

#db_utils.php
<?php

....

function save_profile($player, $args) {
	global $pdo;
  	$params = ["player"=>$player];
		$setStr = "";
  	foreach ($args as $key => $value) {
    		$setStr .= $key . "=" . $pdo->quote($value) . ",";
	}
  	$setStr = rtrim($setStr, ",");
  	$stmt = $pdo->prepare("UPDATE players SET $setStr WHERE username = :player");
  	$stmt -> execute($params);
}

.....

?>

Chức năng save_profile.php sẽ nhận đầu vào của người dùng với mảng ở trên file save_game.php

Sau đó nó sẽ nối vào biến $setStr ví dụ mảng nó có giá trị như sau [”clicks”⇒15, “level”⇒14] thì biến $setStr sẽ như sau: clicks = “15”, level = “14” và nó sẽ đi vào câu truy vấn để update dữ liệu của player:

UPDATE players SET clicks =15, level =14WHERE username = :player

Sau khi mình biết các chức năng thì mình sẽ đi vào khai thác:

Sau khi thực hiện chức năng save_game ở trên ứng dụng mình bắt request

image.png

Sẽ ra sao nếu mình có thể thêm được biến role có value là “Admin” . Nhưng role bị chặn. Thực ra để bypass được cách kiểm tra trên ta chỉ cần thay biến role thành role/**/

image.png

Như vậy có thể bypass được câu lệnh if cũng như có thể thực hiện truy vấn để thay đổi role của mình. Sau khi update xong logout rồi login lại thì nhận có thể truy cập được vào trang admin

image.png

Thực hiện truy cập vào trang admin, ở đây có 1 nút export dẫn tới chức năng export.php

#export.php
<?php
session_start();
include_once("db_utils.php");

if ($_SESSION["ROLE"] != "Admin") {
  header('Location: /index.php');
  die;
}

function random_string($length) {
    $key = '';
    $keys = array_merge(range(0, 9), range('a', 'z'));

    for ($i = 0; $i < $length; $i++) {
        $key .= $keys[array_rand($keys)];
    }

    return $key;
}

$threshold = 1000000;
if (isset($_POST["threshold"]) && is_numeric($_POST["threshold"])) {
    $threshold = $_POST["threshold"];
}
$data = get_top_players($threshold);
$currentplayer = get_current_player($_SESSION["PLAYER"]);
$s = "";
if ($_POST["extension"] == "txt") {
    $s .= "Nickname: ". $currentplayer["nickname"] . " Clicks: " . $currentplayer["clicks"] . " Level: " . $currentplayer["level"] . "\n";
    foreach ($data as $player) {
    $s .= "Nickname: ". $player["nickname"] . " Clicks: " . $player["clicks"] . " Level: " . $player["level"] . "\n";
  }
} elseif ($_POST["extension"] == "json") {
  $s .= json_encode($currentplayer);
  $s .= json_encode($data);
} else {
  $s .= '<table>';
  $s .= '<thead>';
  $s .= '  <tr>';
  $s .= '    <th scope="col">Nickname</th>';
  $s .= '    <th scope="col">Clicks</th>';
  $s .= '    <th scope="col">Level</th>';
  $s .= '  </tr>';
  $s .= '</thead>';
  $s .= '<tbody>';
  $s .= '  <tr>';
  $s .= '    <th scope="row">' . $currentplayer["nickname"] . '</th>';
  $s .= '    <td>' . $currentplayer["clicks"] . '</td>';
  $s .= '    <td>' . $currentplayer["level"] . '</td>';
  $s .= '  </tr>';

  foreach ($data as $player) {
    $s .= '  <tr>';
    $s .= '    <th scope="row">' . $player["nickname"] . '</th>';
    $s .= '    <td>' . $player["clicks"] . '</td>'; 
    $s .= '    <td>' . $player["level"] . '</td>';
    $s .= '  </tr>';
  }
  $s .= '</tbody>';
  $s .= '</table>';
} 

$filename = "exports/top_players_" . random_string(8) . "." . $_POST["extension"];
file_put_contents($filename, $s);
header('Location: /admin.php?msg=Data has been saved in ' . $filename);
?>

Ở trang này có chức năng cho phép xuất ra đuôi file .txt, .json, .html lên đường dẫn exports/top_players_randomStr những người có số điểm clicks lớn hơn $threshhold = 1000000

image.png

Nhưng thực ra mình có thể xuất ra đuôi file nào tuỳ thích và ở đây mình sẽ để đuôi .php

image.png

Sẽ thế nào khi mình có thể thay đổi cái bảng đó thành code php để RCE

Trước hết mình phải cần điểm trên 1000000 thì mình sẽ vào top_player. Ở đây mình chỉ cần sửa biến clicks của mình thành 9999999 thì sẽ được lọt vào top player

image.png

Mình đã lọt vào top player

image.png

Xuất ra file .php

image.png

Mình chỉ cần thay đổi giá trị biến nickname như cách mình làm với Role thành php code là sẽ RCE được

Đầu tiên mình sẽ thay đổi giá trị nickname của mình

image.png

Sau đó thực hiện xuất file php và truy cập vào file php đó

image.png

Như vậy có thể RCE được hệ thống, bây giờ thực hiện reverse shell

image.png

image.png

RCE thành công vào hệ thống

4. Recommendation

  • Không nên cho phép đặt nhiều biến vào 1 array như chức năng save_game
  • Ở chức năng export nên đặt whitelist các tên đuôi file được phép xuất

All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí