Bootstrap

攻防世界 - Web - Level 3 | lottery

关注这个靶场的其它相关笔记:攻防世界(XCTF) —— 靶场笔记合集-CSDN博客

0x01:考点速览

  • 看到题不知道怎么做先扫目录,万一就扫到了 .git 了呢

  • 弱类型比较扩充: true 能等于绝大多数的数字(字符型,浮点型,整型)

0x02:Write UP

本题考查的是 PHP 代码审计,随题附赠的是目标网页的后端代码,当然,如果题目不提供,我们也可以通过目录扫描工具发现目标存在 robots.txt 页面,进而发现目标可能存在 .git 文件泄露,通过工具,我们也同样可以获得目标网页的后端代码。

因为文件中内容比较多,所以就不一个文件一个文件的讲解了,这里只简单的概述一下每个文件大概完成了什么功能:

 目标页面构成如下:
 - index.php     -> 首页,显示游戏规则
 - register.php  -> 注册页面,用于注册用户后购买彩票
 - buy.php   -> 彩票抽奖页面,在此页面可以通过抽奖获得虚拟币
 - account.php   -> 用户账户页面,用于显示用户的账户余额
 - market.php    -> 领奖页面,通过虚拟币购买我们需要的 flag
 - logout.php    -> 登出页面,同时销毁用户 SESSION
  
 目标其他文件功能如下:
 - api.php       -> 所有的逻辑都通过调用此页面完成,包括注册用户,销毁用户,生成彩票随机数,购买 flag 等
 - check_register.php    -> 用于检查用户是否已经注册,如果未注册,则跳转到注册页面
 - config.php        -> 配置文件,里面包含了 flag
 - footer.php            -> 页脚
 - header.php        -> 页头

经过对文件功能的分析,我们可以确定我们的渗透目标为 api.php,如果能找到一串必赢数字,我们就可以很轻松的购买到 flag,下面的内容是对于 api.php 文件的分析。

首先是购买彩票的模块,我们先了解一下彩票购买的流程,代码如下:

 // 彩票购买模块
 function buy($req){
     require_registered();                   // 检测用户是否注册过
     require_min_money(2);                   // 检测用户余额是否大于 2, 如果用户余额不满足最低限度,则会提示 you don't have enough money
  
     $money = $_SESSION['money'];            // $moeny = 用户余额,保存在 Session 中
     $numbers = $req['numbers'];             // $umbers = 用户选择的号码
     $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) {                  // 根据规则计算奖金 -> 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]);
 }

通过分析 api.php 中的 buy () 方法我们找到了如下两个可疑的地方:

  • random_win_nums () -> 最终生成的是否是真随机数,又或者是伪随机数。

  • $numbers [$i] == $win_numbers [$i] -> 使用的是 == 弱类型比较,那么是否有一个东西,能够和所有的或者绝大部分的数字相等。

这里笔者开一下上帝之眼,明确一下第一个可疑的地方是没用的。在 random_win_nums () 函数中目标是通过 7 次调用 openssl_random_pseudo_bytes (10, $cstrong) 函数生成了一个随机字符串。如果百度上面的函数,他会告诉你 openssl_random_pseudo_bytes () 这个函数是用来生成伪随机字节串的。但这里的伪随机字节串不是表明我们能精准的算出每次他生成的结果,而是表明该方法是基于某种算法而不是完全基于物理随机过程。所以计算目标可能产生的结果,这条路就走不通了,那么我们走第二个可疑的地方。

什么东西,通过弱类型比较后,能够与绝大部分字符串版的数字相等呢?

首先,触发弱类型比较需要两个不同类型的数据,目标是单个字符。那我们剩下考虑的范围就在整型,浮点型,布尔型变量中考虑了。

目标又会通过 ​i] 取出一位进行比较,这要怎么做呢,我们知道的,很多纯字符串在低版本 PHP 中因为弱类型比较都会等于 0,即 var_dump (‘hacker’ == 0) -> true;但是现在跟我们比较的是数字的字符,那么通过等于 0 来尝试找到一个万能密码显然是不可能了(浮点数应该没必要考虑了)。

下面将思考方向放到布尔类型的变量中,true or false,很多数字的值都为 true,仅有一个数字的值为 false:

 <?php
  
 for ($number = 0; $number <= 9; $number++) {
     $tmp = strrev($number); // 转换为字符
     echo $tmp . " == true -> ";
     var_dump($tmp == true) . "\n";
 }
  
 /*
 输出结果:
 0 == true -> bool(false)
 1 == true -> bool(true)
 2 == true -> bool(true)
 3 == true -> bool(true)
 4 == true -> bool(true)
 5 == true -> bool(true)
 6 == true -> bool(true)
 7 == true -> bool(true)
 8 == true -> bool(true)
 9 == true -> bool(true)
 */

好的,那么下面,我们来分析一下,什么作为载体,帮我传递这 7 个 true,到目标上呢?

下面的内容是 api.php 中接收请求到调用函数处理的过程,我们可以发现端倪:

 <?php
  
 // 强制,请求方法为 Post ,且传入的数据类型为 Json 格式
 if($_SERVER["REQUEST_METHOD"] != 'POST' || !isset($_SERVER["CONTENT_TYPE"]) || $_SERVER["CONTENT_TYPE"] != 'application/json'){
     response_error('please post json data');
 }
 // 利用 php://input 获取请求体中的数据
 $data = json_decode(file_get_contents('php://input'), true);    
 // 判断 json 是否解析成功 -> 废话,还是说要传入 Json 格式的数据
 if(json_last_error() != JSON_ERROR_NONE){   
     response_error('invalid json');
 }
 // 传入的 Json 字典中存在 action 键值对
 require_keys($data, ['action']);
 // 根据 action 键值对执行不同的操作
 switch ($data['action']) {
     case 'buy':
         require_keys($data, ['numbers']);
         buy($data); // 购买操作还需要 numbers 健
         break;
  
     case 'flag':
         flag($data);
         break;
  
     case 'register':
         require_keys($data, ['name']);
         register($data);
         break;
     
     default:
         response_error('invalid request');
         break;
 }

可以发现,目标并没有检测我们传入的 numbers 是否必须为字符串或者其他类型,那么我们完全可以用列表来传递我们的 Payload,攻击数据包如下(先注册哈):

重放个两三次就好了,然后就可以舒舒服服的拿着 money 去 market 领取 flag 了(如果不够就多点几次,不会有人能抽到全 0 吧):

;