ctfer

knowledge_base_web

2026-09-28 #dev

RCE:远程代码执行

其中对于反序列化,有:

php反序列化:理清楚并利用魔术方法构造出的pop链

python反序列化:

​ 1.找到pickle.loads()的入口和过滤条件

​ 2.生成/手写一段能绕过过滤、执行命令的pickle数据

·ctf中常用pickle的协议0,为纯ASCII文本,可供人类直接读写

T2-ssti注入一个cat /flag,为什么要写成lipsum.globals[‘os’].popen(‘cat /flag’).read()

在模板里从 Jinja2 默认能访问到的对象出发,沿着 Python 对象模型一路找到 os 模块,再执行命令并把输出读回来。

拆开看:

1
{{ lipsum.__globals__['os'].popen('cat /flag').read() }}

1. 为什么不能直接写 os.popen(...)?

在 Jinja2 模板上下文里,默认并没有把 Python 的 os 模块暴露成变量。你直接写:

1
{{ os.popen('cat /flag').read() }}

模板引擎会认为 os 是一个未定义变量,通常报错或渲染为空。SSTI 要做的就是:从模板里能访问到的对象,找到 Python 全局命名空间,再拿到危险模块。

2. lipsum 是什么?

lipsum 是 Jinja2 自带的一个全局函数,用来生成 Lorem Ipsum 文本。默认情况下,Jinja2 模板里可以直接调用:

1
{{ lipsum() }}

所以它是一个模板里“天然可访问”的 Python 函数对象。

3. __globals__ 是什么?

在 Python 中,函数对象有一个属性 __globals__,它返回这个函数定义时所在模块的全局命名空间字典。

lipsum 定义在 Jinja2 的 jinja2.utils 模块里,而这个模块顶部 import os,所以:

1
lipsum.__globals__['os']

就相当于拿到了 jinja2.utils 模块里的 os 模块。

换句话说:

1
2
3
4
lipsum.__globals__
# 是 jinja2.utils 模块的全局变量字典
lipsum.__globals__['os']
# 从中取出 os 模块

4. 为什么用 ['os'] 而不是 .os?

因为 __globals__ 是一个字典。字典取键可以用:

1
d['os']

也可以用属性方式:

1
d.os

但方括号更明确,而且当键名不是合法标识符时必须用方括号。这里写 ['os'] 就是标准的字典取值。

5. 为什么要 .popen(...).read()?

os.popen('cat /flag') 会通过 shell 执行命令,并返回一个类似文件的对象。命令输出在管道里,必须调用 .read() 才能把输出读成字符串。

如果不加 .read():

1
{{ lipsum.__globals__['os'].popen('cat /flag') }}

模板最终渲染出来的可能是类似:

1
<os._wrap_close object at 0x...>

而不是 /flag 的内容。

如果换成 os.system('cat /flag'),它返回的是退出码,命令输出通常直接写到服务器进程的标准输出,不一定进入 HTTP 响应,所以 SSTI 里更常用 popen(...).read()。

6. 整条链路的含义

1
2
3
4
5
6
7
8
9
10
lipsum
→ Jinja2 默认全局函数,模板可直接访问
lipsum.__globals__
→ 函数定义所在模块的全局命名空间
lipsum.__globals__['os']
→ 从全局命名空间里拿到 os 模块
os.popen('cat /flag')
→ 执行命令并拿到管道对象
.read()
→ 读取命令输出,作为模板渲染结果回显

所以它不是固定咒语,而是:找一个模板里能访问到的 Python 函数,用它的 __globals__ 跳到 Python 全局空间,再找 os、subprocess、__builtins__ 等危险对象。

不同环境里可用的“跳板”可能不同,比如 cycler、joiner、namespace、Flask 的 config、url_for 等,都可能被拿来找 __globals__。lipsum 只是因为 Jinja2 默认提供、比较常用。

防御上,根本办法是不要把用户输入当模板渲染,使用 render_template 传变量,启用沙箱,限制模板全局对象,并让 Web 进程最小权限运行。以上仅用于授权测试或 CTF 场景。

T3-XML格式和常见利用

好的!既然题目里出现了 <阵枢> 这样的中文标签,我们正好借此机会把 XML 的“骨架”彻底拆解一遍。

XML 的本质是一种纯文本的、用来结构化存储和传输数据的格式(你可以把它想象成更严格的 JSON)。它必须遵守一套严格的语法规则。

下面我把 XML 的核心构成要素,结合你题目里的标签给你讲透:

1. XML 的五大核心组成部分

组成部分 作用 你的题目里的体现
① 声明(可选) 告诉解析器版本和编码 <?xml version="1.0"?>
② 根元素(必须) 有且仅有一个最外层的包裹标签 <root> 或你自定义的最外层标签
③ 元素(必须) 成对出现的开始和结束标签 <阵枢>内容</阵枢>
④ 属性(可选) 写在开始标签内,key="value" 形式 <阵枢 id="1">
⑤ 实体(特殊) 用来表示特殊字符或引用外部资源 &xxe;(你 Payload 里的变量)

2. 关键语法规则(必须死记,CTF 常考)

  • 必须有唯一的根元素:所有标签必须被一个最外层的父标签包裹。不能写两个并列的顶级标签。
  • 标签必须严格闭合:有 <阵枢> 就必须有 </阵枢>。如果没有内容,可以简写为 <阵枢/>。
  • 标签名区分大小写:<Array> 和 <array> 是两个不同的标签。
  • 属性值必须加引号:可以是单引号或双引号,但必须存在,例如 <解析 状态="成功">。
  • 嵌套不能交叉:<A><B></A></B> 是绝对错误的,必须 <A><B></B></A>(后进先出)。

3. 你的题目为什么可以用“中文标签”?

因为 XML 底层使用 Unicode(UTF-8) 编码,所以标签名、属性名、文本内容都支持中文。所以在后端代码里写 <阵枢> 是完全合法且有效的,你提交的 Payload 会被 PHP 的 DOMDocument 正常解析。


4. 重点来了:DTD(文档类型定义)—— 这是你解题的关键

你在漏洞利用时,肯定看到了 <!DOCTYPE ...> 这串东西。它不属于普通的 XML 数据,而是 XML 的“前置说明书”,用来定义文档的结构规则和实体(变量)。

DTD 内部实体(普通变量):

1
2
3
<!DOCTYPE 根标签 [
<!ENTITY 变量名 "变量值">
]>

引用方式:&变量名;
例如:

1
2
3
4
<!DOCTYPE root [
<!ENTITY author "小张">
]>
<root><阵枢>&author;</阵枢></root>

解析后,<阵枢> 里的内容会变成 “小张”。

DTD 外部实体(你读取文件用的):
这就是你 Payload 里的核心:

1
2
3
<!DOCTYPE root [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>

SYSTEM 关键字告诉解析器:“这个变量的值,不在文本里写死,请去我指定的外部路径(系统文件)去读取内容。”
当你写 <阵枢>&xxe;</阵枢> 时,&xxe; 被替换成了 /etc/passwd 的全部文本内容。


5. 把完整结构串起来(标准 XML 文档全貌)

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>   <!-- ① 声明:告诉版本和编码 -->
<!DOCTYPE 阵枢 [ <!-- ② DTD:定义文档规则/实体 -->
<!ENTITY 变量 "这是一个值">
<!ENTITY 文件 SYSTEM "file:///flag">
]>
<阵枢 属性="示例"> <!-- ③ 根元素(带属性),开始 -->
这里是普通文本,&变量; <!-- 内容中引用内部实体 -->
<解析>状态正常</解析> <!-- 子元素 -->
<输出>&文件;</输出> <!-- 子元素引用外部实体 -->
</阵枢> <!-- ④ 根元素闭合 -->

特别提醒(针对本题环境)

你的题目报错 Start tag expected, '<' not found,说明后端接收到的不是以 < 开头的文本(比如你之前只输了 env)。
由于这个 XML 解析器没有禁用外部实体加载(这是漏洞根源),所以只要你的输入格式完全符合上述 5 条核心规则,它就会乖乖去读文件并把内容吐回给你。

现在再回头看你成功读取 /etc/passwd 的 Payload,是不是每个字符的位置和含义都清晰多了?如果还有哪个标签符号觉得模糊,随时问我。😊

评论
也许会发生一些好事!
分享

评论