下面为github某代码审计项目前两道题的详细解答 challenge1 话不多说,直接上代码:
<?php
$users = array(
"0:9b5c3d2b64b8f74e56edec71462bd97a" ,
"1:4eb5fb1501102508a86971773849d266",
"2:facabd94d57fc9f1e655ef9ce891e86e",
"3:ce3924f011fe323df3a6a95222b0c909",
"4:7f6618422e6a7ca2e939bd83abde402c",
"5:06e2b745f3124f7d670f78eabaa94809",
"6:8e39a6e40900bb0824a8e150c0d0d59f",
"7:d035e1a80bbb377ce1edce42728849f2",
"8:0927d64a71a9d0078c274fc5f4f10821",
"9:e2e23d64a642ee82c7a270c6c76df142",
"10:70298593dd7ada576aff61b6750b9118"
);
$valid_user = false;
$input = $_COOKIE['user'];
$input[1] = md5($input[1]);
foreach ($users as $user)
{
$user = explode(":", $user);
if ($input === $user) {
$uid = $input[0] + 0;
$valid_user = true;
}
}
if (!$valid_user) {
die("not a valid user\n");
}
if ($uid == 0) {
echo "Hello Admin How can I serve you today?\n";
echo "SECRETS ....\n";
} else {
echo "Welcome back user\n";
}
拿到这道题,我们先要找问题的突破点
if ($uid == 0) {
echo "Hello Admin How can I serve you today?\n";
echo "SECRETS ....\n";
从这里,我们可以看到,如果我们要拿到flag,那么uid的值必须为0 那么这个uid是什么呢?
我们继续看前面一段代码
$input = $_COOKIE['user'];
$input[1] = md5($input[1]);
foreach ($users as $user)
{
$user = explode(":", $user);
if ($input === $user) {
$uid = $input[0] + 0;
$valid_user = true;
}
}
我们先传入一个数组user赋值给input 然后将input的第二项进行md5加密 然后对users数组进行遍历,然后判断其是否和通过cookie传入的user数组相等 如果相等的话,就把user数组的第一个元素赋值给uid,并通过加0的方式来进行int型的强制转换 并将valid_user置为true
那么如果我们要得到flag,那么需要满足两个条件: 1.user[0] 的值为 0 2.valid_user = true
结合这两个条件,那么解出这道题的最直接的方法就是得到users数组中第一个用户的密码的明文。 通过前面的
$input[1] = md5($input[1]);
以及
if ($input === $user) {
我们可以得出前面的密文是经过md5加密后的密文 我们尝试通过在线解密的平台来获得该密文对应的明文
通过解密平台并不能得到我们想要的明文,那么我们该怎么办呢?
为了介绍下一种拿到flag的姿势,我们先要谈一个php的漏洞 在php5.6.11之前,php的数组在进行"=="和"==="比较的时候有一个截断漏洞: 就是当数组的键名的值大于2<sup>32</sup>的时候,会造成一种截断,此时,如果键名如果为2<sup>32</sup>那么其实就和0是等价的,如果为2<sup>32</sup>+1,那么其实就和1是等价的,以此类推........ 可能有些读者不是很懂我说的是什么意思 我们可以通过几个demo来更直观的解释 我们先计算2<sup>32</sup>的值:
得到2<sup>32</sup>的值为:4294967296 首先是"=="的比较的demo:
<?php
var_dump([0 => 0] == [4294967296 => 0]);
运行结果:
<?php
var_dump([1 => 0] == [4294967297 => 0]);
运行结果: bool(true)
然后下面是 "==="的demo:
<?php
var_dump([0 => 0] === [4294967296 => 0]);
运行结果: bool(true)
<?php
var_dump([1 => 0] === [4294967297 => 0]);
运行结果: bool(true)
但是我们需要注意的是,这个漏洞只针对键名,而数组键所对应的值则不存在。 demo如下:
<?php
var_dump([0 => 0] === [0 => 4294967296 ]);
我们可以看到,值的2<sup>32</sup>就不和0相等了
好了,这个漏洞就介绍到这里了,现在我们再回到我们面临的问题中来。 我们知道的怎样 才能拿到flag,可是users列表中第一个用户密码的明文我们拿不到。
这时我们就要用另外一种思路了,uid为0,不一定就是匹配到了users列表中的第一位用户。如果我们的user[0]没有值,又匹配到了users列表中的任意一个用户,不就可以达到uid为0,valid_user为true吗
但是问题又来了,user[0]要不存在值,那么又怎么能满足匹配到users列表中的用户呢? 这时就要利用我们前面提到的php在数组比较时产生的漏洞了 我们再看这段代码
foreach ($users as $user)
{
$user = explode(":", $user);
if ($input === $user) {
$uid = $input[0] + 0;
$valid_user = true;
}
}
在比较的时候,input和user这两个数组时用的"=="在比较,那么我们可以不设置input[0]的值,而是用input[4294967296]来代替,然后比较的时候时用的input[4294967296]和input[1]来和列表中的用户比较,而uid又是通过input[0]+0来得到的,这样就完美绕过了它的限制。 通过对上面users列表中用户的哈希进行md5解密,可以发现5号用户的密码明文时可以得到的:
然后我们完整的思路就是令 user[1] = "hund" user[4294967296] = "5"就可以了
challenge2
<?php
show_source(__FILE__);
$flag = "xxxx";
if(isset($_GET['time'])){
if(!is_numeric($_GET['time'])){
echo 'The time must be number.';
}else if($_GET['time'] < 60 * 60 * 24 * 30 * 2){
echo 'This time is too short.';
}else if($_GET['time'] > 60 * 60 * 24 * 30 * 3){
echo 'This time is too long.';
}else{
sleep((int)$_GET['time']);
echo $flag;
}
echo '<hr>';
}
分析该题,传入一个get类型的变量time,然后判断time的大小,如果time的大小位于区间[5184000,7776000]的话就可以得到flag。 但是这时就出现一个问题了:
}else{
sleep((int)$_GET['time']);
echo $flag;
}
在得到flag之前,会将time转换为int型,并作为sleep函数的参数,也就是会停顿我们输入的time那么多秒 那么我们要得到flag,岂不是要等待这么久吗,这样等下去,ctf比赛早就结束了。 所以我们想要得到flag,就的想办法绕过
sleep((int)$_GET['time']);
那么该怎么绕过呢,这时我们就要从它将time强制转换为int型这里来做手脚了
1.首先,我们要知道,在php中,通过get和post传过来的参数的值都是字符串型的 2.php中,数与数之间可以用不同的进制来进行比较
我们可以做下demo来验证下: 我们知道这里的time要大于5184000且小于7776000。 那么我们就选择一个5184001来做实验吧 先将5184001转换为16进制
然后再来和5184000和7776000进行比较:
<?php
echo (5184000 < 0x4f1a01);
echo "<br><br>";
echo (7776000 > 0x4f1a01);
运行结果:
从返回结果来看,十六进制和十进制这些时可以直接比较的。
十进制的数字字符串可以直接强制转换为int型而不会出现错误什么的,那么如果是十六进制的来进行int型的强制转换呢?
<?php
$time = "5184001";
echo (int)$time;
echo "<br><br>";
$time_hex = "0x4f1a01";
echo (int)$time_hex;
返回结果:
我们可以看到,十进制正常转换,而十六进制在强制转换的时候,当识别到第一个0后后面的就无法识别的,最终转换的结果为0。 那么我们的解题思路就出来了: 利用十六进制可以与十进制进行比较,而在进行int强制转换时的结果为0就可以绕过sleep拿到flag了