这其实是我学了CTF的Web方向一个月以来做的第一道关于git泄露的题,之前发了几篇关于CTFHub中git泄露的题目,是因为我遇到这题的时候完全知识盲区,去补了git知识然后相应的做了CTFHub的题目之后才敢来继续做这题。总的来说,这题还是很好的,考了git泄露的知识点,考了代码审计,考了php弱类型比较的知识点,还考了burp抓包相关的知识,这题虽然前前后后算上学习居然有一天,但是对于我这样的小白来说,收获还是巨大的。
打开题目发现是个猜数字游戏,登录之后玩了玩发现买flag遥不可及,直接dirsearch扫描,发现存在git泄露。
使用githack之后如果文件夹中除了.git文件没有其他文件,就需要在bash里使用git checkout-index -a,这样就可以看到网页的各种php文件了。
然后就是代码审计,我把所有php文件都看了一遍,除去看不懂的JavaScript,大致的逻辑便懂了,但是我这里犯了一个特别重大的错误,因为我以前C语言写的比较多,而且是刚学Web一个月,接触的PHP代码不多,虽然以前遇到过PHP中弱类型比较的相关问题,但是我一看到==,就下意识按照C语言的那种思路,觉得就是判断是不是相等,没有往弱类型比较上面想,导致我接下来的全都走偏了。
错误解法:
我把整个文件复制到了我自己的wamp下面,然后开wamp打开这个文件在本地运行,我把api里面的代码做了修改,无论数字有几个相等,都获得最大金额,然后买flag,出现了这个情况:
this is not the real flag这句话我在config.php里面看到过,当时我看到的时候以为这只是迷惑我的,并不是真正的buy那里的接口,我觉得git总能以某种我不知道的方法让我获得flag.事实证明我错了,还错的很离谱。为此我一定要把这次错误的经历写下来,让自己以后少犯这样的错误。
正确解法:
我们首先需要对api.php文件进行代码审计
require_once('config.php');
header('Content-Type: application/json');
function response($resp){
die(json_encode($resp));
}
function response_error($msg){
$result = ['status'=>'error'];
$result['msg'] = $msg;
response($result);
}
function require_keys($req, $keys){
foreach ($keys as $key) {
if(!array_key_exists($key, $req)){
response_error('invalid request');
}
}
}
function require_registered(){
if(!isset($_SESSION['name']) || !isset($_SESSION['money'])){
response_error('register first');
}
}
function require_min_money($min_money){
if(!isset($_SESSION['money'])){
response_error('register first');
}
$money = $_SESSION['money'];
if($money < 0){
$_SESSION = array();
session_destroy();
response_error('invalid negative money');
}
if($money < $min_money){
response_error('you don\' have enough money');
}
}
if($_SERVER["REQUEST_METHOD"] != 'POST' || !isset($_SERVER["CONTENT_TYPE"]) || $_SERVER["CONTENT_TYPE"] != 'application/json'){
response_error('please post json data');
}
$data = json_decode(file_get_contents('php://input'), true);
if(json_last_error() != JSON_ERROR_NONE){
response_error('invalid json');
}
require_keys($data, ['action']);
// my boss told me to use cryptographically secure algorithm
function random_num(){
do {
$byte = openssl_random_pseudo_bytes(10, $cstrong);
$num = ord($byte);
} while ($num >= 250);
if(!$cstrong){
response_error('server need be checked, tell admin');
}
$num /= 25;
return strval(floor($num));
}
function random_win_nums(){
$result = '';
for($i=0; $i<7; $i++){
$result .= random_num();
}
return $result;
}
function buy($req){
require_registered();
require_min_money(2);
$money = $_SESSION['money'];
$numbers = $req['numbers'];
$win_numbers = random_win_nums();
$same_count = 0;
for($i=0; $i<7; $i++){
if($numbers[$i] == $win_numbers[$i]){
$same_count++;
}
}
switch ($same_count) {
case 2:
$prize = 5;
break;
case 3:
$prize = 20;
break;
case 4:
$prize = 300;
break;
case 5:
$prize = 1800;
break;
case 6:
$prize = 200000;
break;
case 7:
$prize = 5000000;
break;
default:
$prize = 0;
break;
}
$money += $prize - 2;
$_SESSION['money'] = $money;
response(['status'=>'ok','numbers'=>$numbers, 'win_numbers'=>$win_numbers, 'money'=>$money, 'prize'=>$prize]);
}
function flag($req){
global $flag;
global $flag_price;
require_registered();
$money = $_SESSION['money'];
if($money < $flag_price){
response_error('you don\' have enough money');
} else {
$money -= $flag_price;
$_SESSION['money'] = $money;
$msg = 'Here is your flag: ' . $flag;
response(['status'=>'ok','msg'=>$msg, 'money'=>$money]);
}
}
function register($req){
$name = $req['name'];
$_SESSION['name'] = $name;
$_SESSION['money'] = 20;
response(['status'=>'ok']);
}
switch ($data['action']) {
case 'buy':
require_keys($data, ['numbers']);
buy($data);
break;
case 'flag':
flag($data);
break;
case 'register':
require_keys($data, ['name']);
register($data);
break;
default:
response_error('invalid request');
break;
}
重点就是buy函数那里。我们发现(我没发现)
for($i=0; $i<7; $i++){
if($numbers[$i] == $win_numbers[$i]){
$same_count++;
}
}
这里使用了PHP的弱类型比较,这就导致了漏洞的产生。
PHP中==和 ===是不一样的,==会把比较的双方经过类型转换后再比较,但是
===
是比较严格的比较,如果类型不同就会返回false。
重点来了,怎么样才能获得最大金额呢?这里使用bool欺骗。
PHP的"=="和JS中有”= ="在进行比较时,如果有true和false参与,规则会不同。
在php中,如果bool和"任何其他类型"比较,"任何其他类型"会转换为bool。
在JS中,
在本题中,使用加密然后随机来产生七位数字,这是在一个字符串中的,然后一个一个的取出进行比较,那么我们还需要知道下面这个知识点:
PHP中
当转换为 boolean 时,以下值被认为是 FALSE :
布尔值 FALSE 本身
整型值 0(零)
浮点型值 0.0(零)
空字符串,以及字符串 “0”
不包括任何元素的数组(注意,一旦包含元素,就算包含的元素只是一个空数组,也是true)
不包括任何成员变量的对象(仅 PHP 4.0 适用)
特殊类型 NULL(包括尚未赋值的变量)
从空标记生成的 SimpleXML 对象
所有其它值都被认为是 TRUE (包括任何资源)。
因此,根据上面所说的,挨个比较的时候,如果使用bool欺骗,那么例如字符串"1","2"等,都将转换为true与我们输入的bool值进行比较,这样就可以利用这个漏洞。
因此,我们先随便输入7个数字,然后burp抓包,将numbers改为全为true的。
这里还需要注意的是我们看到content-type里面的是application/json。
application/json 这个 Content-Type 作为响应头大家肯定不陌生。实际上,现在越来越多的人把它作为请求头,用来告诉服务端消息主体是序列化后的 JSON 字符串。
而json又是支持bool类型的,因此可以修改提交,然后多次提交然后买flag就可以了。
这题其实关键还是PHP弱类型比较。PHP弱类型比较的题型还是比较多的,网上可以看到很多,但是单纯的看也没什么收获,只能这样遇到一次这样的题目记住一次方法,这样不断积累,对于PHP弱类型比较会掌握的比较好了。