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 | lipsum.__globals__ |
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 | lipsum |
所以它不是固定咒语,而是:找一个模板里能访问到的 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 |
引用方式:&变量名;
例如:
1 |
|
解析后,<阵枢> 里的内容会变成 “小张”。
DTD 外部实体(你读取文件用的):
这就是你 Payload 里的核心:
1 |
SYSTEM 关键字告诉解析器:“这个变量的值,不在文本里写死,请去我指定的外部路径(系统文件)去读取内容。”
当你写 <阵枢>&xxe;</阵枢> 时,&xxe; 被替换成了 /etc/passwd 的全部文本内容。
5. 把完整结构串起来(标准 XML 文档全貌)
1 | <!-- ① 声明:告诉版本和编码 --> |
特别提醒(针对本题环境)
你的题目报错 Start tag expected, '<' not found,说明后端接收到的不是以 < 开头的文本(比如你之前只输了 env)。
由于这个 XML 解析器没有禁用外部实体加载(这是漏洞根源),所以只要你的输入格式完全符合上述 5 条核心规则,它就会乖乖去读文件并把内容吐回给你。
现在再回头看你成功读取 /etc/passwd 的 Payload,是不是每个字符的位置和含义都清晰多了?如果还有哪个标签符号觉得模糊,随时问我。😊
评论