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
Thì nó là 1 game với số điểm bằng lượng click của người chơi
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 = “14” WHERE 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
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/**/
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
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
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
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
Mình đã lọt vào top player
Xuất ra file .php
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
Sau đó thực hiện xuất file php và truy cập vào file php đó
Như vậy có thể RCE được hệ thống, bây giờ thực hiện reverse shell
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