攻防世界Web 高手进阶区:Lottery

前言

这其实是我学了CTF的Web方向一个月以来做的第一道关于git泄露的题,之前发了几篇关于CTFHub中git泄露的题目,是因为我遇到这题的时候完全知识盲区,去补了git知识然后相应的做了CTFHub的题目之后才敢来继续做这题。总的来说,这题还是很好的,考了git泄露的知识点,考了代码审计,考了php弱类型比较的知识点,还考了burp抓包相关的知识,这题虽然前前后后算上学习居然有一天,但是对于我这样的小白来说,收获还是巨大的。

WP

打开题目发现是个猜数字游戏,登录之后玩了玩发现买flag遥不可及,直接dirsearch扫描,发现存在git泄露。
使用githack之后如果文件夹中除了.git文件没有其他文件,就需要在bash里使用git checkout-index -a,这样就可以看到网页的各种php文件了。
然后就是代码审计,我把所有php文件都看了一遍,除去看不懂的JavaScript,大致的逻辑便懂了,但是我这里犯了一个特别重大的错误,因为我以前C语言写的比较多,而且是刚学Web一个月,接触的PHP代码不多,虽然以前遇到过PHP中弱类型比较的相关问题,但是我一看到==,就下意识按照C语言的那种思路,觉得就是判断是不是相等,没有往弱类型比较上面想,导致我接下来的全都走偏了。

错误解法:
我把整个文件复制到了我自己的wamp下面,然后开wamp打开这个文件在本地运行,我把api里面的代码做了修改,无论数字有几个相等,都获得最大金额,然后买flag,出现了这个情况:
攻防世界Web 高手进阶区:Lottery_第1张图片

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中,

  1. 如果有一个操作数是布尔值,则在比较相等性之前先将其转换为数值——false转换为0,而true转换为1;
  2. 如果一个操作数是字符串,另一个操作数是数值,在比较相等性之前先将字符串转换为数值;
  3. 如果一个操作数是对象,另一个操作数不是,则调用对象的valueOf()方法,用得到的基本类型值按照前面的规则进行比较

在本题中,使用加密然后随机来产生七位数字,这是在一个字符串中的,然后一个一个的取出进行比较,那么我们还需要知道下面这个知识点:
PHP中
当转换为 boolean 时,以下值被认为是 FALSE :
布尔值 FALSE 本身
整型值 0(零)
浮点型值 0.0(零)
空字符串,以及字符串 “0”
不包括任何元素的数组(注意,一旦包含元素,就算包含的元素只是一个空数组,也是true)
不包括任何成员变量的对象(仅 PHP 4.0 适用)
特殊类型 NULL(包括尚未赋值的变量)
从空标记生成的 SimpleXML 对象
所有其它值都被认为是 TRUE (包括任何资源)。

因此,根据上面所说的,挨个比较的时候,如果使用bool欺骗,那么例如字符串"1","2"等,都将转换为true与我们输入的bool值进行比较,这样就可以利用这个漏洞。

因此,我们先随便输入7个数字,然后burp抓包,将numbers改为全为true的。

攻防世界Web 高手进阶区:Lottery_第2张图片
这里还需要注意的是我们看到content-type里面的是application/json。
application/json 这个 Content-Type 作为响应头大家肯定不陌生。实际上,现在越来越多的人把它作为请求头,用来告诉服务端消息主体是序列化后的 JSON 字符串。
而json又是支持bool类型的,因此可以修改提交,然后多次提交然后买flag就可以了。

后记

这题其实关键还是PHP弱类型比较。PHP弱类型比较的题型还是比较多的,网上可以看到很多,但是单纯的看也没什么收获,只能这样遇到一次这样的题目记住一次方法,这样不断积累,对于PHP弱类型比较会掌握的比较好了。

你可能感兴趣的:(泄露,PHP弱类型比较,代码审计,php,后端,安全)